Add Chain 138 Snap: deploy/verify scripts, runbook, CI, security, version/health, token list validation
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
9
chain138-snap/.editorconfig
Normal file
9
chain138-snap/.editorconfig
Normal file
@@ -0,0 +1,9 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
42
chain138-snap/.eslintrc.js
Normal file
42
chain138-snap/.eslintrc.js
Normal file
@@ -0,0 +1,42 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
},
|
||||
|
||||
extends: ['@metamask/eslint-config'],
|
||||
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.js'],
|
||||
extends: ['@metamask/eslint-config-nodejs'],
|
||||
},
|
||||
|
||||
{
|
||||
files: ['*.ts', '*.tsx'],
|
||||
extends: ['@metamask/eslint-config-typescript'],
|
||||
},
|
||||
|
||||
{
|
||||
files: ['*.test.ts', '*.test.js'],
|
||||
extends: ['@metamask/eslint-config-jest'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-shadow': [
|
||||
'error',
|
||||
{ allow: ['describe', 'expect', 'it'] },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
ignorePatterns: [
|
||||
'!.prettierrc.js',
|
||||
'**/!.eslintrc.js',
|
||||
'**/dist*/',
|
||||
'**/*__GENERATED__*',
|
||||
'**/build',
|
||||
'**/public',
|
||||
'**/.cache',
|
||||
],
|
||||
};
|
||||
4
chain138-snap/.github/CODEOWNERS
vendored
Normal file
4
chain138-snap/.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# Lines starting with '#' are comments.
|
||||
# Each line is a file pattern followed by one or more owners.
|
||||
|
||||
* @MetaMask/snaps-devs
|
||||
15
chain138-snap/.github/dependabot.yml
vendored
Normal file
15
chain138-snap/.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: 'npm'
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: 'daily'
|
||||
time: '06:00'
|
||||
allow:
|
||||
- dependency-name: '@metamask/*'
|
||||
target-branch: 'main'
|
||||
versioning-strategy: 'increase-if-necessary'
|
||||
open-pull-requests-limit: 10
|
||||
110
chain138-snap/.github/workflows/build-lint-test.yml
vendored
Normal file
110
chain138-snap/.github/workflows/build-lint-test.yml
vendored
Normal file
@@ -0,0 +1,110 @@
|
||||
name: Build, Lint, and Test
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
name: Prepare
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout and setup environment
|
||||
uses: MetaMask/action-checkout-and-setup@v1
|
||||
with:
|
||||
is-high-risk-environment: false
|
||||
cache-node-modules: true
|
||||
- name: Enable pnpm
|
||||
run: corepack enable && corepack prepare pnpm@9.15.0 --activate
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- prepare
|
||||
steps:
|
||||
- name: Checkout and setup environment
|
||||
uses: MetaMask/action-checkout-and-setup@v1
|
||||
with:
|
||||
is-high-risk-environment: false
|
||||
- name: Enable pnpm
|
||||
run: corepack enable && corepack prepare pnpm@9.15.0 --activate
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
- name: Upload Snap build artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: snap-${{ runner.os }}-${{ github.sha }}
|
||||
path: ./packages/snap/dist
|
||||
retention-days: 1
|
||||
- name: Require clean working directory
|
||||
shell: bash
|
||||
run: |
|
||||
if ! git diff --exit-code; then
|
||||
echo "Working tree dirty at end of job"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- prepare
|
||||
steps:
|
||||
- name: Checkout and setup environment
|
||||
uses: MetaMask/action-checkout-and-setup@v1
|
||||
with:
|
||||
is-high-risk-environment: false
|
||||
- name: Enable pnpm
|
||||
run: corepack enable && corepack prepare pnpm@9.15.0 --activate
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Lint
|
||||
run: pnpm run lint
|
||||
- name: Require clean working directory
|
||||
shell: bash
|
||||
run: |
|
||||
if ! git diff --exit-code; then
|
||||
echo "Working tree dirty at end of job"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
e2e-test:
|
||||
name: End-to-end Test
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- prepare
|
||||
- build
|
||||
steps:
|
||||
- name: Checkout and setup environment
|
||||
uses: MetaMask/action-checkout-and-setup@v1
|
||||
with:
|
||||
is-high-risk-environment: false
|
||||
- name: Download Snap build artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: snap-${{ runner.os }}-${{ github.sha }}
|
||||
path: ./packages/snap/dist
|
||||
- name: Enable pnpm
|
||||
run: corepack enable && corepack prepare pnpm@9.15.0 --activate
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Run Snap unit tests
|
||||
run: pnpm --filter snap run test
|
||||
- name: Install Playwright browsers
|
||||
run: npx playwright install chromium --with-deps
|
||||
- name: Run Playwright E2E (companion site)
|
||||
run: pnpm run test:e2e
|
||||
env:
|
||||
CI: true
|
||||
timeout-minutes: 8
|
||||
- name: Require clean working directory
|
||||
shell: bash
|
||||
run: |
|
||||
if ! git diff --exit-code; then
|
||||
echo "Working tree dirty at end of job"
|
||||
exit 1
|
||||
fi
|
||||
49
chain138-snap/.github/workflows/deploy-snap-site.yml
vendored
Normal file
49
chain138-snap/.github/workflows/deploy-snap-site.yml
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
# 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.
|
||||
# Optional: set secret GATSBY_SNAP_API_BASE_URL for production API in build.
|
||||
name: Deploy Snap Site
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
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:
|
||||
|
||||
env:
|
||||
GATSBY_PATH_PREFIX: /snap
|
||||
|
||||
jobs:
|
||||
build-and-verify:
|
||||
name: Build site and verify
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Enable pnpm
|
||||
run: corepack enable && corepack prepare pnpm@9.15.0 --activate
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build Snap site (pathPrefix /snap)
|
||||
run: pnpm --filter site run build
|
||||
env:
|
||||
GATSBY_PATH_PREFIX: /snap
|
||||
GATSBY_SNAP_API_BASE_URL: ${{ secrets.GATSBY_SNAP_API_BASE_URL }}
|
||||
|
||||
- name: Upload site artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
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 }}"
|
||||
53
chain138-snap/.github/workflows/main.yml
vendored
Normal file
53
chain138-snap/.github/workflows/main.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
name: Main
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
check-workflows:
|
||||
name: Check workflows
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout and setup environment
|
||||
uses: MetaMask/action-checkout-and-setup@v1
|
||||
with:
|
||||
is-high-risk-environment: false
|
||||
- name: Download actionlint
|
||||
id: download-actionlint
|
||||
run: bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/7fdc9630cc360ea1a469eed64ac6d78caeda1234/scripts/download-actionlint.bash) 1.6.23
|
||||
shell: bash
|
||||
- name: Check workflow files
|
||||
run: ${{ steps.download-actionlint.outputs.executable }} -color
|
||||
shell: bash
|
||||
|
||||
build-lint-test:
|
||||
name: Build, lint, and test
|
||||
uses: ./.github/workflows/build-lint-test.yml
|
||||
|
||||
all-jobs-completed:
|
||||
name: All jobs completed
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- check-workflows
|
||||
- build-lint-test
|
||||
outputs:
|
||||
PASSED: ${{ steps.set-output.outputs.PASSED }}
|
||||
steps:
|
||||
- name: Set PASSED output
|
||||
id: set-output
|
||||
run: echo "PASSED=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
all-jobs-pass:
|
||||
name: All jobs pass
|
||||
if: ${{ always() }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: all-jobs-completed
|
||||
steps:
|
||||
- name: Check that all jobs have passed
|
||||
run: |
|
||||
passed="${{ needs.all-jobs-completed.outputs.PASSED }}"
|
||||
if [[ $passed != "true" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
46
chain138-snap/.github/workflows/security-code-scanner.yml
vendored
Normal file
46
chain138-snap/.github/workflows/security-code-scanner.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
name: MetaMask Security Code Scanner
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
run-security-scan:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
steps:
|
||||
- name: MetaMask Security Code Scanner
|
||||
uses: MetaMask/action-security-code-scanner@v1
|
||||
with:
|
||||
repo: ${{ github.repository }}
|
||||
paths_ignored: |
|
||||
.storybook/
|
||||
'**/__snapshots__/'
|
||||
'**/*.snap'
|
||||
'**/*.stories.js'
|
||||
'**/*.stories.tsx'
|
||||
'**/*.test.browser.ts*'
|
||||
'**/*.test.js*'
|
||||
'**/*.test.ts*'
|
||||
'**/fixtures/'
|
||||
'**/jest.config.js'
|
||||
'**/jest.environment.js'
|
||||
'**/mocks/'
|
||||
'**/test*/'
|
||||
docs/
|
||||
e2e/
|
||||
merged-packages/
|
||||
node_modules
|
||||
storybook/
|
||||
test*/
|
||||
rules_excluded: example
|
||||
project_metrics_token: ${{ secrets.SECURITY_SCAN_METRICS_TOKEN }}
|
||||
slack_webhook: ${{ secrets.APPSEC_BOT_SLACK_WEBHOOK }}
|
||||
82
chain138-snap/.gitignore
vendored
Normal file
82
chain138-snap/.gitignore
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
.DS_Store
|
||||
dist/
|
||||
build/
|
||||
coverage/
|
||||
.cache/
|
||||
public/
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# Playwright
|
||||
test-results/
|
||||
playwright-report/
|
||||
playwright/.cache/
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
5
chain138-snap/.npmrc
Normal file
5
chain138-snap/.npmrc
Normal file
@@ -0,0 +1,5 @@
|
||||
# pnpm (default package manager). Yarn is also supported.
|
||||
auto-install-peers=true
|
||||
strict-peer-dependencies=false
|
||||
# Use pnpm-lock.yaml; do not upgrade to npm/yarn lockfiles
|
||||
package-lock=false
|
||||
1
chain138-snap/.nvmrc
Normal file
1
chain138-snap/.nvmrc
Normal file
@@ -0,0 +1 @@
|
||||
lts/*
|
||||
11
chain138-snap/.prettierrc.mjs
Normal file
11
chain138-snap/.prettierrc.mjs
Normal file
@@ -0,0 +1,11 @@
|
||||
// All of these are defaults except singleQuote, but we specify them
|
||||
// for explicitness
|
||||
const config = {
|
||||
quoteProps: 'as-needed',
|
||||
singleQuote: true,
|
||||
tabWidth: 2,
|
||||
trailingComma: 'all',
|
||||
plugins: ['prettier-plugin-packagejson'],
|
||||
};
|
||||
|
||||
export default config;
|
||||
8
chain138-snap/.vscode/settings.json
vendored
Normal file
8
chain138-snap/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"json.schemas": [
|
||||
{
|
||||
"fileMatch": ["snap.manifest.json"],
|
||||
"url": "https://raw.githubusercontent.com/MetaMask/SIPs/main/assets/sip-9/snap.manifest.schema.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
9
chain138-snap/.yarn/plugins/@yarnpkg/plugin-allow-scripts.cjs
vendored
Normal file
9
chain138-snap/.yarn/plugins/@yarnpkg/plugin-allow-scripts.cjs
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
/* eslint-disable */
|
||||
//prettier-ignore
|
||||
module.exports = {
|
||||
name: "@yarnpkg/plugin-allow-scripts",
|
||||
factory: function (require) {
|
||||
var plugin=(()=>{var l=Object.defineProperty;var s=Object.getOwnPropertyDescriptor;var a=Object.getOwnPropertyNames;var c=Object.prototype.hasOwnProperty;var p=(t=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(t,{get:(o,e)=>(typeof require<"u"?require:o)[e]}):t)(function(t){if(typeof require<"u")return require.apply(this,arguments);throw new Error('Dynamic require of "'+t+'" is not supported')});var u=(t,o)=>{for(var e in o)l(t,e,{get:o[e],enumerable:!0})},f=(t,o,e,r)=>{if(o&&typeof o=="object"||typeof o=="function")for(let i of a(o))!c.call(t,i)&&i!==e&&l(t,i,{get:()=>o[i],enumerable:!(r=s(o,i))||r.enumerable});return t};var m=t=>f(l({},"__esModule",{value:!0}),t);var g={};u(g,{default:()=>d});var n=p("@yarnpkg/shell"),x={hooks:{afterAllInstalled:async()=>{let t=await(0,n.execute)("yarn run allow-scripts");t!==0&&process.exit(t)}}},d=x;return m(g);})();
|
||||
return plugin;
|
||||
}
|
||||
};
|
||||
28
chain138-snap/.yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
vendored
Normal file
28
chain138-snap/.yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
vendored
Normal file
File diff suppressed because one or more lines are too long
786
chain138-snap/.yarn/releases/yarn-3.2.1.cjs
vendored
Executable file
786
chain138-snap/.yarn/releases/yarn-3.2.1.cjs
vendored
Executable file
File diff suppressed because one or more lines are too long
17
chain138-snap/.yarnrc.yml
Normal file
17
chain138-snap/.yarnrc.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
enableScripts: false
|
||||
|
||||
enableTelemetry: 0
|
||||
|
||||
logFilters:
|
||||
- code: YN0004
|
||||
level: discard
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
plugins:
|
||||
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
|
||||
spec: '@yarnpkg/plugin-workspace-tools'
|
||||
- path: .yarn/plugins/@yarnpkg/plugin-allow-scripts.cjs
|
||||
spec: 'https://raw.githubusercontent.com/LavaMoat/LavaMoat/main/packages/yarn-plugin-allow-scripts/bundles/@yarnpkg/plugin-allow-scripts.js'
|
||||
|
||||
yarnPath: .yarn/releases/yarn-3.2.1.cjs
|
||||
129
chain138-snap/DEPLOY_VMID5000.md
Normal file
129
chain138-snap/DEPLOY_VMID5000.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# 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.
|
||||
136
chain138-snap/E2E_PREPARATION.md
Normal file
136
chain138-snap/E2E_PREPARATION.md
Normal file
@@ -0,0 +1,136 @@
|
||||
# E2E Preparation — Token-Aggregation and Companion Site
|
||||
|
||||
Complete these steps so that full end-to-end testing (market data, bridge routes, swap quotes) succeeds.
|
||||
|
||||
---
|
||||
|
||||
## 1. Token-aggregation service
|
||||
|
||||
The Snap and companion site call the token-aggregation API for networks, token list, bridge routes, and swap quotes. You need the service running and reachable.
|
||||
|
||||
### Option A: Local (recommended for E2E)
|
||||
|
||||
1. **Prerequisites**
|
||||
- Node.js 20+
|
||||
- PostgreSQL 14+ with TimescaleDB
|
||||
- RPC URLs for Chain 138 and 651940 (e.g. from `.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
|
||||
```
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
4. **Run**
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
npm run dev
|
||||
```
|
||||
Service will listen on **http://localhost:3000**.
|
||||
Verify: `curl http://localhost:3000/health` and `curl http://localhost:3000/api/v1/networks`.
|
||||
|
||||
### Option B: Docker
|
||||
|
||||
```bash
|
||||
cd smom-dbis-138/services/token-aggregation
|
||||
# Ensure .env exists with DATABASE_URL etc.
|
||||
docker-compose up -d
|
||||
# API on http://localhost:3000
|
||||
```
|
||||
|
||||
### Option C: Deployed / staging
|
||||
|
||||
Use your deployed token-aggregation base URL (e.g. `https://your-token-aggregation-api.com`). Ensure CORS allows the Snap/site origin and the endpoints below respond:
|
||||
|
||||
- `GET /api/v1/networks`
|
||||
- `GET /api/v1/report/token-list`
|
||||
- `GET /api/v1/bridge/routes`
|
||||
- `GET /api/v1/quote`
|
||||
- `GET /api/v1/tokens` (for market summary)
|
||||
|
||||
---
|
||||
|
||||
## 2. Companion site environment
|
||||
|
||||
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
|
||||
cp .env.production.dist .env
|
||||
# or .env.production for production build
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
3. **Restart the site** if it is already running so the variable is picked up (Gatsby reads env at build/start).
|
||||
|
||||
---
|
||||
|
||||
## 3. Run Snap + site
|
||||
|
||||
From the Chain 138 Snap monorepo root:
|
||||
|
||||
```bash
|
||||
cd metamask-integration/chain138-snap
|
||||
pnpm run start
|
||||
```
|
||||
|
||||
- Site and Snap are served at **http://localhost:8000**.
|
||||
- Open that URL in a browser where **MetaMask Flask** is installed.
|
||||
|
||||
---
|
||||
|
||||
## 4. Manual E2E checklist
|
||||
|
||||
Use the [E2E testing checklist (MetaMask Flask)](TESTING_INSTRUCTIONS.md#e2e-testing-checklist-metamask-flask) in `TESTING_INSTRUCTIONS.md` and complete every item (environment, install Snap, RPC methods, companion site cards).
|
||||
|
||||
---
|
||||
|
||||
## 5. Optional: automated E2E (Playwright)
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
- First time: run `npx playwright install` to install browser binaries.
|
||||
- `test:e2e` starts the dev server if needed and runs Playwright against http://localhost:8000.
|
||||
- The **site must compile successfully** for E2E to pass (if Gatsby fails to build, fix the dev environment first).
|
||||
- It does **not** drive MetaMask Flask; for full install/connect flow you must run the manual checklist.
|
||||
- Optional: `pnpm run test:e2e:ui` to open the Playwright UI.
|
||||
|
||||
---
|
||||
|
||||
## 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/):
|
||||
|
||||
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.
|
||||
|
||||
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`.
|
||||
|
||||
---
|
||||
|
||||
**Last updated:** 2026-02-11
|
||||
201
chain138-snap/LICENSE.APACHE2
Normal file
201
chain138-snap/LICENSE.APACHE2
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2022 ConsenSys Software Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
16
chain138-snap/LICENSE.MIT0
Normal file
16
chain138-snap/LICENSE.MIT0
Normal file
@@ -0,0 +1,16 @@
|
||||
MIT No Attribution
|
||||
|
||||
Copyright 2022 ConsenSys Software Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
software and associated documentation files (the "Software"), to deal in the Software
|
||||
without restriction, including without limitation the rights to use, copy, modify,
|
||||
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
29
chain138-snap/PACKAGE_MANAGER.md
Normal file
29
chain138-snap/PACKAGE_MANAGER.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Package manager
|
||||
|
||||
**Default:** [pnpm](https://pnpm.io) (fast, disk-efficient, single lockfile).
|
||||
|
||||
**Preferred alternative:** [Yarn](https://yarnpkg.com) is supported; use it if you prefer.
|
||||
|
||||
## Setup
|
||||
|
||||
1. **pnpm (default)**
|
||||
- Ensure [Corepack](https://nodejs.org/api/corepack.html) is enabled: `corepack enable`
|
||||
- Install: `pnpm install`
|
||||
- Scripts: `pnpm run build`, `pnpm run start`, `pnpm run test`, etc.
|
||||
- Commit `pnpm-lock.yaml` so CI and others use the same dependency tree.
|
||||
|
||||
2. **Yarn (alternative)**
|
||||
- Use existing `.yarnrc.yml` and Yarn 3.2.1.
|
||||
- Install: `yarn install`
|
||||
- Scripts: `yarn build`, `yarn start`, `yarn test`, etc.
|
||||
- Root `packageManager` is set to pnpm; if you use Yarn only, run `yarn install` and ignore `pnpm-lock.yaml`.
|
||||
|
||||
## CI
|
||||
|
||||
GitHub Actions use **pnpm** by default: `corepack prepare pnpm@9.15.0 --activate` then `pnpm install --frozen-lockfile` and `pnpm run build` / `pnpm run lint` / `pnpm --filter snap run test`. **Before the first CI run** (or after adding dependencies), run `pnpm install` locally and commit `pnpm-lock.yaml` so CI has a lockfile.
|
||||
|
||||
## Recommendations
|
||||
|
||||
- Use **pnpm** for consistency with CI and docs.
|
||||
- Use **Yarn** if your tooling or team already standardizes on it; scripts in `package.json` are written for pnpm but Yarn can run the same targets (`yarn build`, etc.) from the root.
|
||||
- Do not mix lockfiles in the same branch: use either `pnpm-lock.yaml` or `yarn.lock`, not both, to avoid drift.
|
||||
65
chain138-snap/README.md
Normal file
65
chain138-snap/README.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# 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.
|
||||
|
||||
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.
|
||||
|
||||
**Integrators:** Market data, swap quote, and bridge route features require the dApp to pass `apiBaseUrl` (the token-aggregation service base URL) when invoking the Snap. Set `GATSBY_SNAP_API_BASE_URL` on the companion site so the demo page works.
|
||||
|
||||
This Snap targets the **latest stable MetaMask Snap SDK** (`@metamask/snaps-sdk`).
|
||||
|
||||
## Snaps is pre-release software
|
||||
|
||||
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.
|
||||
|
||||
## Getting Started
|
||||
|
||||
Clone the template-snap repository [using this template](https://github.com/MetaMask/template-snap-monorepo/generate)
|
||||
and set up the development environment.
|
||||
|
||||
**Default (pnpm):**
|
||||
|
||||
```shell
|
||||
pnpm install && pnpm start
|
||||
```
|
||||
|
||||
**Alternative (yarn):**
|
||||
|
||||
```shell
|
||||
yarn install && yarn start
|
||||
```
|
||||
|
||||
See [PACKAGE_MANAGER.md](PACKAGE_MANAGER.md) for details.
|
||||
|
||||
## Cloning
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
**pnpm (default):** `pnpm run test`, `pnpm run lint`, `pnpm run lint:fix`
|
||||
**yarn:** `yarn test`, `yarn lint`, `yarn lint:fix`
|
||||
|
||||
- **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.
|
||||
49
chain138-snap/RUNBOOK.md
Normal file
49
chain138-snap/RUNBOOK.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# 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`.
|
||||
33
chain138-snap/SECURITY.md
Normal file
33
chain138-snap/SECURITY.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Security
|
||||
|
||||
## 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.
|
||||
|
||||
## Token-aggregation API (public)
|
||||
|
||||
If the token-aggregation API is publicly reachable:
|
||||
|
||||
- **CORS:** Configure allowed origins; avoid `*` in production if you need to restrict callers.
|
||||
- **Rate limiting:** Consider rate limiting per IP or per key to reduce abuse.
|
||||
- **Secrets:** Do not put API keys or admin credentials in the Snap or companion site frontend. Use env or a secrets manager for deploy/CI.
|
||||
|
||||
## Snap permissions
|
||||
|
||||
The Snap requests minimal permissions. Current (see `packages/snap/snap.manifest.json`):
|
||||
|
||||
- **snap_dialog:** Show dialogs (e.g. bridge routes, token list URL).
|
||||
- **endowment:rpc** (dapps: true, snaps: false): Handle RPC from dApps.
|
||||
- **endowment:network-access:** Fetch token list, bridge routes, market data from the configured API/URLs.
|
||||
|
||||
Only add permissions that the Snap actually needs. Document why each permission is required (e.g. in this file or in the Snap description).
|
||||
|
||||
## Dependency and supply chain
|
||||
|
||||
- Keep dependencies up to date (`pnpm update`, Renovate/Dependabot).
|
||||
- Run `pnpm run lint` and tests before release. Watch for MetaMask Snaps API changes.
|
||||
|
||||
## Reporting
|
||||
|
||||
Report security issues privately (e.g. maintainer contact or security policy) rather than in public issues.
|
||||
386
chain138-snap/TESTING_INSTRUCTIONS.md
Normal file
386
chain138-snap/TESTING_INSTRUCTIONS.md
Normal file
@@ -0,0 +1,386 @@
|
||||
# Chain 138 Snap Testing Instructions
|
||||
|
||||
**Date:** 2026-01-30
|
||||
**Status:** Built and ready for testing
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **MetaMask Flask** (development version of MetaMask)
|
||||
- Download: https://metamask.io/flask/
|
||||
- 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
|
||||
```
|
||||
(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
|
||||
|
||||
3. **For full E2E (API-dependent features):** Token-aggregation service and companion site env. See [E2E Preparation](E2E_PREPARATION.md).
|
||||
|
||||
---
|
||||
|
||||
## E2E Preparation
|
||||
|
||||
For full end-to-end success (market data, bridge routes, swap quotes), complete these before running the checklist:
|
||||
|
||||
1. **Start token-aggregation** (local or use a deployed URL).
|
||||
- See [E2E_PREPARATION.md](E2E_PREPARATION.md) for steps (database, env, `npm run dev` or Docker).
|
||||
- Note the API base URL (e.g. `http://localhost:3000` for local).
|
||||
|
||||
2. **Configure companion site env.**
|
||||
- In `packages/site`, copy `.env.production.dist` to `.env` or `.env.production`.
|
||||
- Set `GATSBY_SNAP_API_BASE_URL` to the token-aggregation base URL (e.g. `http://localhost:3000`).
|
||||
- Restart the site if it is already running so the variable is picked up.
|
||||
|
||||
3. **Run Snap + site:** From repo root, `pnpm run start` (serves site and Snap on http://localhost:8000).
|
||||
|
||||
4. **Install MetaMask Flask** and use the checklist in [E2E testing checklist (MetaMask Flask)](#e2e-testing-checklist-metamask-flask) below.
|
||||
|
||||
**Automated E2E (optional):** Run `pnpm run test:e2e` to start the dev server (if needed) and run Playwright tests against the companion site. See [E2E_PREPARATION.md](E2E_PREPARATION.md#5-optional-automated-e2e-playwright). This does not drive MetaMask Flask.
|
||||
|
||||
---
|
||||
|
||||
## Testing Steps
|
||||
|
||||
### 1. Install the Snap
|
||||
|
||||
1. Open browser with MetaMask Flask installed
|
||||
2. Navigate to **http://localhost:8000**
|
||||
3. You should see the Snap installation page
|
||||
4. Click **"Connect"** to install the Snap
|
||||
5. MetaMask Flask will prompt for permissions - approve them
|
||||
|
||||
### 2. Test RPC Methods
|
||||
|
||||
Once installed, you can test the Snap's RPC methods:
|
||||
|
||||
#### Test `get_networks`
|
||||
|
||||
Pass `apiBaseUrl` (your token-aggregation service URL). Open browser console:
|
||||
|
||||
```javascript
|
||||
await ethereum.request({
|
||||
method: 'wallet_invokeSnap',
|
||||
params: {
|
||||
snapId: 'local:http://localhost:8000',
|
||||
request: {
|
||||
method: 'get_networks',
|
||||
params: { apiBaseUrl: 'https://your-token-aggregation-api.com' },
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**Expected response:** `{ version, networks: [ ... ] }` with full EIP-3085 params for Chain 138, Ethereum Mainnet, and ALL Mainnet.
|
||||
|
||||
**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' }
|
||||
```
|
||||
|
||||
#### Test `get_chain138_config`
|
||||
|
||||
Requires `apiBaseUrl`. Returns Chain 138 config from the API:
|
||||
|
||||
```javascript
|
||||
await ethereum.request({
|
||||
method: 'wallet_invokeSnap',
|
||||
params: {
|
||||
snapId: 'local:http://localhost:8000',
|
||||
request: {
|
||||
method: 'get_chain138_config',
|
||||
params: { apiBaseUrl: 'https://your-token-aggregation-api.com' },
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**Expected response:** Chain 138 params (chainId, chainName, rpcUrls, nativeCurrency, blockExplorerUrls, oracles).
|
||||
|
||||
**Optional:** Pass `networksUrl` instead of `apiBaseUrl` to use a remote networks JSON.
|
||||
|
||||
#### Test `get_chain138_market_chains`
|
||||
|
||||
```javascript
|
||||
await ethereum.request({
|
||||
method: 'wallet_invokeSnap',
|
||||
params: {
|
||||
snapId: 'local:http://localhost:8000',
|
||||
request: {
|
||||
method: 'get_chain138_market_chains',
|
||||
params: {
|
||||
apiBaseUrl: 'https://your-token-aggregation-api.com', // When deployed
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**Expected response:**
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"chainId": 138,
|
||||
"name": "DeFi Oracle Meta Mainnet",
|
||||
"nativeToken": { "symbol": "ETH", "decimals": 18 },
|
||||
"rpcUrl": "https://rpc-http-pub.d-bis.org",
|
||||
"explorerUrl": "https://explorer.d-bis.org"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### Test `get_market_summary`
|
||||
|
||||
Requires `apiBaseUrl` (token-aggregation service URL). Fetches tokens with optional market data (price, volume) for a chain. Optional `chainId` (default 138).
|
||||
|
||||
```javascript
|
||||
await ethereum.request({
|
||||
method: 'wallet_invokeSnap',
|
||||
params: {
|
||||
snapId: 'local:http://localhost:8000',
|
||||
request: {
|
||||
method: 'get_market_summary',
|
||||
params: {
|
||||
apiBaseUrl: 'https://your-token-aggregation-api.com',
|
||||
chainId: 138, // optional, default 138
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**Expected response:** `{ tokens: [ { symbol, name, address, market?: { priceUsd, volume24h } } ] }` or `{ error, tokens: [] }` on failure.
|
||||
|
||||
#### Test `show_market_data`
|
||||
|
||||
Requires `apiBaseUrl`. Opens a Snap dialog listing token symbols and USD prices from the token-aggregation API.
|
||||
|
||||
```javascript
|
||||
await ethereum.request({
|
||||
method: 'wallet_invokeSnap',
|
||||
params: {
|
||||
snapId: 'local:http://localhost:8000',
|
||||
request: {
|
||||
method: 'show_market_data',
|
||||
params: {
|
||||
apiBaseUrl: 'https://your-token-aggregation-api.com',
|
||||
chainId: 138, // optional
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**Expected:** A MetaMask dialog showing "Market data (Chain 138)" and token lines with prices. Without `apiBaseUrl`, the Snap shows an alert asking to pass it.
|
||||
|
||||
#### Test `get_token_list` and `get_token_list_url`
|
||||
|
||||
With `apiBaseUrl`: same pattern as above; the Snap calls `${apiBaseUrl}/api/v1/report/token-list` (optional `chainId` in params).
|
||||
**Optional:** Pass `tokenListUrl` to fetch the token list from a JSON URL (e.g. GitHub raw):
|
||||
|
||||
```javascript
|
||||
params: { tokenListUrl: 'https://raw.githubusercontent.com/org/repo/main/token-list.json', chainId: 138 }
|
||||
```
|
||||
|
||||
#### Test `get_bridge_routes`
|
||||
|
||||
Requires `apiBaseUrl` or `bridgeListUrl`. Returns CCIP bridge routes (WETH9 / WETH10) and Chain 138 bridge addresses.
|
||||
|
||||
```javascript
|
||||
await ethereum.request({
|
||||
method: 'wallet_invokeSnap',
|
||||
params: {
|
||||
snapId: 'local:http://localhost:8000',
|
||||
request: {
|
||||
method: 'get_bridge_routes',
|
||||
params: { apiBaseUrl: 'https://your-token-aggregation-api.com' },
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**Expected response:** `{ routes: { weth9: {...}, weth10: {...} }, chain138Bridges: { weth9, weth10 } }`.
|
||||
|
||||
**Optional:** Pass `bridgeListUrl` instead of `apiBaseUrl` to fetch bridge list from a JSON URL.
|
||||
|
||||
#### Test `show_bridge_routes`
|
||||
|
||||
Requires `apiBaseUrl` or `bridgeListUrl`. Opens a Snap dialog with bridge route summary (WETH9/WETH10 → Ethereum Mainnet).
|
||||
|
||||
```javascript
|
||||
await ethereum.request({
|
||||
method: 'wallet_invokeSnap',
|
||||
params: {
|
||||
snapId: 'local:http://localhost:8000',
|
||||
request: {
|
||||
method: 'show_bridge_routes',
|
||||
params: { apiBaseUrl: 'https://your-token-aggregation-api.com' },
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
#### Test `get_swap_quote`
|
||||
|
||||
Requires `apiBaseUrl`, `tokenIn`, `tokenOut`, `amountIn` (raw amount string). Optional `chainId` (default 138).
|
||||
|
||||
```javascript
|
||||
await ethereum.request({
|
||||
method: 'wallet_invokeSnap',
|
||||
params: {
|
||||
snapId: 'local:http://localhost:8000',
|
||||
request: {
|
||||
method: 'get_swap_quote',
|
||||
params: {
|
||||
apiBaseUrl: 'https://your-token-aggregation-api.com',
|
||||
chainId: 138,
|
||||
tokenIn: '0x...',
|
||||
tokenOut: '0x...',
|
||||
amountIn: '1000000000000000000',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**Expected response:** `{ amountOut: string | undefined, error?: string, poolAddress?: string }`.
|
||||
|
||||
#### Test `show_swap_quote`
|
||||
|
||||
Same params as `get_swap_quote`. Opens a Snap dialog with the quote (In/Out raw amounts).
|
||||
|
||||
#### Test `hello` (basic test)
|
||||
|
||||
```javascript
|
||||
await ethereum.request({
|
||||
method: 'wallet_invokeSnap',
|
||||
params: {
|
||||
snapId: 'local:http://localhost:8000',
|
||||
request: {
|
||||
method: 'hello',
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**Expected response:**
|
||||
|
||||
```json
|
||||
"Hello from Chain 138 Snap!"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## E2E testing checklist (MetaMask Flask)
|
||||
|
||||
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`
|
||||
- [ ] 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**
|
||||
- [ ] Open http://localhost:8000 in the browser
|
||||
- [ ] Click **Connect** and approve Snap installation in MetaMask Flask
|
||||
|
||||
3. **RPC methods (apiBaseUrl or optional networksUrl / tokenListUrl / bridgeListUrl)**
|
||||
- [ ] `hello`
|
||||
- [ ] `get_networks` (apiBaseUrl or networksUrl)
|
||||
- [ ] `get_chain138_config` (apiBaseUrl or networksUrl)
|
||||
- [ ] `get_chain138_market_chains` (apiBaseUrl)
|
||||
- [ ] `get_token_list`, `get_token_list_url` (apiBaseUrl or tokenListUrl; optional chainId)
|
||||
- [ ] `get_oracles` (apiBaseUrl), `show_dynamic_info` (apiBaseUrl or networksUrl/tokenListUrl)
|
||||
- [ ] `get_market_summary`, `show_market_data` (apiBaseUrl; optional chainId)
|
||||
- [ ] `get_bridge_routes`, `show_bridge_routes` (apiBaseUrl or bridgeListUrl)
|
||||
- [ ] `get_swap_quote`, `show_swap_quote` (apiBaseUrl, tokenIn, tokenOut, amountIn; optional chainId)
|
||||
|
||||
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.
|
||||
- [ ] **Swap quote:** Enter token In/Out addresses and amount (raw), then "Get quote" shows amountOut; "Show quote in Snap" opens dialog.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Snap not appearing in MetaMask Flask
|
||||
|
||||
- Ensure dev server is running on port 8000
|
||||
- Check browser console for errors
|
||||
- Try refreshing the page
|
||||
|
||||
### Permission errors
|
||||
|
||||
- Snap needs `endowment:network-access` for API calls
|
||||
- Check `snap.manifest.json` has correct permissions
|
||||
|
||||
### API calls failing
|
||||
|
||||
- Ensure `apiBaseUrl` is provided for methods that need it, or use the optional URL params: `networksUrl`, `tokenListUrl`, `bridgeListUrl` (see RPC method sections above).
|
||||
- On the companion site, set `GATSBY_SNAP_API_BASE_URL` in `.env` or `.env.production` (see `packages/site/.env.production.dist`) so Market data and other API-dependent cards work.
|
||||
- Check CORS settings on the token-aggregation API server (it uses `cors()` by default).
|
||||
- Verify API endpoint is accessible (e.g. token-aggregation and, for full testing, bridge/quote endpoints).
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
### After Testing
|
||||
|
||||
1. **Fix any bugs** found during testing
|
||||
2. **Submit to Snap directory** when ready (see Publishing below)
|
||||
|
||||
### Publishing
|
||||
|
||||
**Checklist before publishing:**
|
||||
|
||||
- [ ] 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).
|
||||
- [ ] `packages/snap/package.json`: `name` and `publishConfig` (e.g. `"access": "public"`) correct for npm.
|
||||
- [ ] Integrator docs updated: dApps/site must pass `apiBaseUrl` (or optional `networksUrl` / `tokenListUrl` / `bridgeListUrl`) for market data, bridge, and swap quote.
|
||||
|
||||
**Snap directory submission checklist:**
|
||||
|
||||
- [ ] All manual E2E checklist items in this doc completed and passing.
|
||||
- [ ] Snap built with `pnpm run build`; `prepublishOnly` has run (manifest shasum updated).
|
||||
- [ ] `packages/snap/package.json`: `name`, `version`, `publishConfig` (e.g. `"access": "public"`) correct for npm.
|
||||
- [ ] `snap.manifest.json`: `description`, `proposedName`, `initialPermissions` match what the Snap uses; `source.location.npm` will be valid after publish.
|
||||
- [ ] Snap package published to npm so the manifest npm source is resolvable.
|
||||
- [ ] [MetaMask Snap publishing guide](https://docs.metamask.io/snaps/how-to/publish-a-snap/) followed: register Snap (package name or bundle URL), description, category, permissions.
|
||||
- [ ] Integrator docs: dApps/site must pass `apiBaseUrl` (or optional `networksUrl` / `tokenListUrl` / `bridgeListUrl`) for market data, bridge, swap quote.
|
||||
|
||||
**Steps to publish to MetaMask Snap directory:**
|
||||
|
||||
1. **Build:** Run `pnpm run build` (or **yarn build**; see [PACKAGE_MANAGER.md](PACKAGE_MANAGER.md)). The `prepublishOnly` script updates the manifest shasum.
|
||||
2. **Publish package:** Publish the Snap package to npm (e.g. from `packages/snap`) so `source.location.npm` in `snap.manifest.json` is valid.
|
||||
3. **Snap directory:** Follow the [MetaMask Snap publishing guide](https://docs.metamask.io/snaps/how-to/publish-a-snap/) to register the Snap (package name or bundle URL, description, category, permissions).
|
||||
|
||||
**Production use:** After the Snap is published, the production Snap ID will be the npm package name (e.g. `npm:snap` or `npm:@your-org/chain138-snap`). For market data, swap quote, and bridge routes to work, dApps (and the companion site) must pass `apiBaseUrl` (your token-aggregation service URL) or the optional URLs when invoking the Snap. Document this for integrators; see "API calls failing" in Troubleshooting.
|
||||
|
||||
---
|
||||
|
||||
## Snap Capabilities
|
||||
|
||||
### Current Features
|
||||
|
||||
- ✅ Get Chain 138 configuration (`get_chain138_config`, `get_networks`)
|
||||
- ✅ 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
|
||||
- ✅ Swap quote (`get_swap_quote`, `show_swap_quote`) when quote API is available
|
||||
|
||||
---
|
||||
|
||||
**Last updated:** 2026-02-11
|
||||
**Status:** Ready for manual testing in MetaMask Flask; Playwright E2E available via `pnpm run test:e2e`
|
||||
17
chain138-snap/e2e/site.spec.ts
Normal file
17
chain138-snap/e2e/site.spec.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
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 });
|
||||
});
|
||||
|
||||
test('page has Snap-related content', async ({ page }) => {
|
||||
await page.goto('/', { waitUntil: 'domcontentloaded' });
|
||||
const body = page.locator('body');
|
||||
await expect(body).toContainText(/Connect|template-snap|Get started|Snap|Install|Reconnect/i, { timeout: 30_000 });
|
||||
});
|
||||
});
|
||||
69
chain138-snap/eslint.config.mjs
Normal file
69
chain138-snap/eslint.config.mjs
Normal file
@@ -0,0 +1,69 @@
|
||||
import base, { createConfig } from '@metamask/eslint-config';
|
||||
import browser from '@metamask/eslint-config-browser';
|
||||
import jest from '@metamask/eslint-config-jest';
|
||||
import nodejs from '@metamask/eslint-config-nodejs';
|
||||
import typescript from '@metamask/eslint-config-typescript';
|
||||
|
||||
const config = createConfig([
|
||||
{
|
||||
ignores: [
|
||||
'**/build/',
|
||||
'**/.cache/',
|
||||
'**/dist/',
|
||||
'**/docs/',
|
||||
'**/public/',
|
||||
'.yarn/',
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
extends: base,
|
||||
|
||||
languageOptions: {
|
||||
sourceType: 'module',
|
||||
parserOptions: {
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
|
||||
settings: {
|
||||
'import-x/extensions': ['.js', '.mjs'],
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
extends: typescript,
|
||||
|
||||
rules: {
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/no-shadow': ['error', { allow: ['Text'] }],
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
files: ['**/*.js', '**/*.cjs', 'packages/snap/snap.config.ts'],
|
||||
extends: nodejs,
|
||||
|
||||
languageOptions: {
|
||||
sourceType: 'script',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
files: ['**/*.test.ts', '**/*.test.tsx', '**/*.test.js'],
|
||||
extends: [jest, nodejs],
|
||||
|
||||
rules: {
|
||||
'@typescript-eslint/unbound-method': 'off',
|
||||
'jest/unbound-method': 'off',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
files: ['packages/site/src/**'],
|
||||
extends: [browser],
|
||||
},
|
||||
]);
|
||||
|
||||
export default config;
|
||||
91
chain138-snap/package.json
Normal file
91
chain138-snap/package.json
Normal file
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"name": "root",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "",
|
||||
"homepage": "https://github.com/MetaMask/template-snap-monorepo#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/MetaMask/template-snap-monorepo/issues"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/MetaMask/template-snap-monorepo.git"
|
||||
},
|
||||
"license": "(MIT-0 OR Apache-2.0)",
|
||||
"author": "",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
"allow-scripts": "pnpm exec allow-scripts",
|
||||
"build": "pnpm -r run build",
|
||||
"lint": "pnpm run lint:eslint && pnpm run lint:misc --check",
|
||||
"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",
|
||||
"start": "pnpm -r --parallel run start",
|
||||
"test": "pnpm --filter snap run build && pnpm --filter snap run test",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:ui": "playwright test --ui"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lavamoat/allow-scripts": "^3.4.2",
|
||||
"@metamask/eslint-config": "^15.0.0",
|
||||
"@metamask/eslint-config-browser": "^15.0.0",
|
||||
"@metamask/eslint-config-jest": "^15.0.0",
|
||||
"@metamask/eslint-config-nodejs": "^15.0.0",
|
||||
"@metamask/eslint-config-typescript": "^15.0.0",
|
||||
"@playwright/test": "^1.58.2",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-import-resolver-typescript": "^4.4.4",
|
||||
"eslint-plugin-import-x": "^4.16.1",
|
||||
"eslint-plugin-jest": "^28.8.3",
|
||||
"eslint-plugin-jsdoc": "^50.2.4",
|
||||
"eslint-plugin-n": "^17.23.2",
|
||||
"eslint-plugin-prettier": "^5.5.5",
|
||||
"eslint-plugin-promise": "^7.2.1",
|
||||
"prettier": "^3.8.1",
|
||||
"prettier-plugin-packagejson": "^3.0.0",
|
||||
"sharp": "^0.34.5",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.54.0"
|
||||
},
|
||||
"packageManager": "pnpm@10.28.2",
|
||||
"engines": {
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"overrides": {
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"lavamoat": {
|
||||
"allowScripts": {
|
||||
"sharp": true
|
||||
}
|
||||
}
|
||||
}
|
||||
14
chain138-snap/packages/site/.env.production.dist
Normal file
14
chain138-snap/packages/site/.env.production.dist
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Copy to .env or .env.production and set values.
|
||||
* Required for E2E: GATSBY_SNAP_API_BASE_URL so Market data, Bridge, and Swap quote cards work.
|
||||
*/
|
||||
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.
|
||||
GATSBY_SNAP_API_BASE_URL=
|
||||
|
||||
# Optional: build version shown in footer and in /snap/version.json (e.g. git short SHA).
|
||||
# GATSBY_BUILD_SHA=abc1234
|
||||
# GATSBY_APP_VERSION=1.0.0
|
||||
43
chain138-snap/packages/site/README.md
Normal file
43
chain138-snap/packages/site/README.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# TypeScript Example Snap Front-end
|
||||
|
||||
This project was bootstrapped with [Gatsby](https://www.gatsbyjs.com/).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run (from repo root: **pnpm** is default, **yarn** is supported):
|
||||
|
||||
### `pnpm run start` (or `yarn start`)
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:8000](http://localhost:8000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.\
|
||||
You will also see any lint errors in the console.
|
||||
|
||||
### `pnpm run build` (or `yarn build`)
|
||||
|
||||
Builds the app for production to the `public` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://www.gatsbyjs.com/docs/how-to/previews-deploys-hosting/) for more information.
|
||||
|
||||
## Environment variables
|
||||
|
||||
Gatsby has built-in support for loading environment variables into the browser and Functions. Loading environment variables into Node.js requires a small code snippet.
|
||||
|
||||
In development, Gatsby will load environment variables from a file named `.env.development`. For builds, it will load from `.env.production`.
|
||||
|
||||
By default you can use the `SNAP_ORIGIN` variable (used in `src/config/snap.ts`) to define a production origin for you snap (eg. `npm:MyPackageName`). If not defined it will defaults to `local:http://localhost:8080`.
|
||||
|
||||
A `.env` file template is available, to use it rename `.env.production.dist` to `.env.production`
|
||||
|
||||
To learn more visit [Gatsby documentation](https://www.gatsbyjs.com/docs/how-to/local-development/environment-variables/)
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Gatsby documentation](https://www.gatsbyjs.com/docs/).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
17
chain138-snap/packages/site/gatsby-browser.tsx
Normal file
17
chain138-snap/packages/site/gatsby-browser.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { GatsbyBrowser } from 'gatsby';
|
||||
import { StrictMode } from 'react';
|
||||
|
||||
import { App } from './src/App';
|
||||
import { Root } from './src/Root';
|
||||
|
||||
export const wrapRootElement: GatsbyBrowser['wrapRootElement'] = ({
|
||||
element,
|
||||
}) => (
|
||||
<StrictMode>
|
||||
<Root>{element}</Root>
|
||||
</StrictMode>
|
||||
);
|
||||
|
||||
export const wrapPageElement: GatsbyBrowser['wrapPageElement'] = ({
|
||||
element,
|
||||
}) => <App>{element}</App>;
|
||||
27
chain138-snap/packages/site/gatsby-config.ts
Normal file
27
chain138-snap/packages/site/gatsby-config.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { GatsbyConfig } from 'gatsby';
|
||||
|
||||
const config: GatsbyConfig = {
|
||||
// When deployed under explorer at /snap/, set pathPrefix so assets load correctly.
|
||||
pathPrefix: process.env.GATSBY_PATH_PREFIX || '/',
|
||||
// This is required to make use of the React 17+ JSX transform.
|
||||
jsxRuntime: 'automatic',
|
||||
|
||||
plugins: [
|
||||
'gatsby-plugin-svgr',
|
||||
'gatsby-plugin-styled-components',
|
||||
{
|
||||
resolve: 'gatsby-plugin-manifest',
|
||||
options: {
|
||||
name: 'Template 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',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
15
chain138-snap/packages/site/gatsby-ssr.tsx
Normal file
15
chain138-snap/packages/site/gatsby-ssr.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { GatsbySSR } from 'gatsby';
|
||||
import { StrictMode } from 'react';
|
||||
|
||||
import { App } from './src/App';
|
||||
import { Root } from './src/Root';
|
||||
|
||||
export const wrapRootElement: GatsbySSR['wrapRootElement'] = ({ element }) => (
|
||||
<StrictMode>
|
||||
<Root>{element}</Root>
|
||||
</StrictMode>
|
||||
);
|
||||
|
||||
export const wrapPageElement: GatsbySSR['wrapPageElement'] = ({ element }) => (
|
||||
<App>{element}</App>
|
||||
);
|
||||
58
chain138-snap/packages/site/package.json
Normal file
58
chain138-snap/packages/site/package.json
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"name": "site",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"license": "(MIT-0 OR Apache-2.0)",
|
||||
"scripts": {
|
||||
"allow-scripts": "pnpm --filter root run allow-scripts",
|
||||
"prebuild": "node -e \"const fs=require('fs');const p=require('path');const d=p.join(__dirname,'static');if(!fs.existsSync(d))fs.mkdirSync(d,{recursive:true});const v=process.env.GATSBY_BUILD_SHA||process.env.GATSBY_APP_VERSION||'dev';fs.writeFileSync(p.join(d,'version.json'),JSON.stringify({version:v,buildTime:new Date().toISOString()}));\"",
|
||||
"build": "GATSBY_TELEMETRY_DISABLED=1 gatsby build",
|
||||
"clean": "rimraf public",
|
||||
"lint": "pnpm run lint:eslint && pnpm run lint:misc --check",
|
||||
"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",
|
||||
"start": "GATSBY_TELEMETRY_DISABLED=1 gatsby develop"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@metamask/providers": "^22.1.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-is": "^18.3.1",
|
||||
"styled-components": "^5.3.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@svgr/webpack": "^6.5.1",
|
||||
"@testing-library/dom": "^8.20.1",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/react": "^18.2.4",
|
||||
"@types/react-dom": "^18.2.4",
|
||||
"@types/styled-components": "^5.1.36",
|
||||
"caniuse-lite": "^1.0.30001767",
|
||||
"eslint": "^9.39.2",
|
||||
"gatsby": "^5.16.0",
|
||||
"gatsby-plugin-manifest": "^5.16.0",
|
||||
"gatsby-plugin-styled-components": "^6.14.0",
|
||||
"gatsby-plugin-svgr": "3.0.0-beta.0",
|
||||
"rimraf": "^5.0.5",
|
||||
"typescript": "~5.9.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.6.0"
|
||||
}
|
||||
}
|
||||
34
chain138-snap/packages/site/src/App.tsx
Normal file
34
chain138-snap/packages/site/src/App.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { FunctionComponent, ReactNode } from 'react';
|
||||
import { useContext } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Footer, Header } from './components';
|
||||
import { GlobalStyle } from './config/theme';
|
||||
import { ToggleThemeContext } from './Root';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
max-width: 100vw;
|
||||
`;
|
||||
|
||||
export type AppProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const App: FunctionComponent<AppProps> = ({ children }) => {
|
||||
const toggleTheme = useContext(ToggleThemeContext);
|
||||
|
||||
return (
|
||||
<>
|
||||
<GlobalStyle />
|
||||
<Wrapper>
|
||||
<Header handleToggleClick={toggleTheme} />
|
||||
{children}
|
||||
<Footer />
|
||||
</Wrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
34
chain138-snap/packages/site/src/Root.tsx
Normal file
34
chain138-snap/packages/site/src/Root.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { FunctionComponent, ReactNode } from 'react';
|
||||
import { createContext, useState } from 'react';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
|
||||
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,
|
||||
);
|
||||
|
||||
export const Root: FunctionComponent<RootProps> = ({ children }) => {
|
||||
const [darkTheme, setDarkTheme] = useState(getThemePreference());
|
||||
|
||||
const toggleTheme: ToggleTheme = () => {
|
||||
setLocalStorage('theme', darkTheme ? 'light' : 'dark');
|
||||
setDarkTheme(!darkTheme);
|
||||
};
|
||||
|
||||
return (
|
||||
<ToggleThemeContext.Provider value={toggleTheme}>
|
||||
<ThemeProvider theme={darkTheme ? dark : light}>
|
||||
<MetaMaskProvider>{children}</MetaMaskProvider>
|
||||
</ThemeProvider>
|
||||
</ToggleThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
123
chain138-snap/packages/site/src/assets/flask_fox.svg
Normal file
123
chain138-snap/packages/site/src/assets/flask_fox.svg
Normal file
@@ -0,0 +1,123 @@
|
||||
<svg width="21" height="18" viewBox="0 0 21 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.7263 9.8073L12.0438 5.43543L13.339 2.32193H7.79883L9.09409 5.43543L9.41153 9.8073L9.51117 11.1859L9.51812 14.5793H11.6174L11.6244 11.1859L11.7263 9.8073Z" fill="url(#paint0_linear_205_1983)"/>
|
||||
<path d="M18.7542 8.96532L14.7896 7.80643L15.9875 9.61763L14.1987 13.1036L16.5645 13.0735H20.0819L18.7542 8.96532Z" fill="url(#paint1_linear_205_1983)"/>
|
||||
<path d="M6.35065 7.80643L2.38609 8.96532L1.06766 13.0735H4.58502L6.94383 13.1036L5.15503 9.61763L6.35065 7.80643Z" fill="url(#paint2_linear_205_1983)"/>
|
||||
<path d="M12.1203 11.9979L11.6198 14.5817L11.9836 14.8315L14.1964 13.1035L14.2636 11.3687L12.1203 11.9979Z" fill="url(#paint3_linear_205_1983)"/>
|
||||
<path d="M6.88132 11.3687L6.94156 13.1035L9.15439 14.8315L9.51818 14.5817L9.01768 11.9979L6.88132 11.3687Z" fill="url(#paint4_linear_205_1983)"/>
|
||||
<path d="M19.5952 5.98827L20.2324 2.89559L19.2778 0.0249634L11.9836 5.43543L14.7061 7.87581L18.7541 8.96762L19.6254 7.9452L19.2453 7.67225L19.8524 7.11941L19.389 6.75624L19.9961 6.29361L19.5952 5.98827Z" fill="url(#paint5_linear_205_1983)"/>
|
||||
<path d="M1.54501 5.98827L0.907806 2.89559L1.86245 0.0249634L9.15669 5.43543L6.43409 7.87581L2.38612 8.9653L1.51489 7.94289L1.89489 7.66994L1.28781 7.11709L1.75123 6.75393L1.14415 6.2913L1.54501 5.98827Z" fill="url(#paint6_linear_205_1983)"/>
|
||||
<path d="M5.15271 9.61761L6.88127 11.3687L6.94151 13.1035L5.15271 9.61761Z" fill="url(#paint7_linear_205_1983)"/>
|
||||
<path d="M15.9875 9.61761L14.1987 13.1035L14.2659 11.3687L15.9875 9.61761Z" fill="url(#paint8_linear_205_1983)"/>
|
||||
<path d="M14.5532 16.0806L11.9836 14.8315L12.1875 16.5062L12.1643 17.2117L14.5532 16.0806Z" fill="#FF9F5A"/>
|
||||
<path d="M6.58707 16.0805L8.976 17.2094L8.95978 16.5039L9.15673 14.8291L6.58707 16.0805Z" fill="#FF9F5A"/>
|
||||
<path d="M16.5621 13.0735L14.5532 16.0806L18.8514 17.2626L20.0795 13.0735H16.5621Z" fill="url(#paint9_linear_205_1983)"/>
|
||||
<path d="M1.06766 13.0735L2.28877 17.2626L6.58699 16.0806L4.58502 13.0735H1.06766Z" fill="url(#paint10_linear_205_1983)"/>
|
||||
<path d="M1.86243 0.0249634L9.15666 5.43543L8.01201 2.32193L1.86243 0.0249634Z" fill="url(#paint11_linear_205_1983)"/>
|
||||
<path d="M13.1282 2.32193L11.9836 5.43543L19.2778 0.0249634L13.1282 2.32193Z" fill="url(#paint12_linear_205_1983)"/>
|
||||
<path d="M6.35065 7.80644L5.15271 9.61764L9.41386 9.80732L9.15666 5.43546L6.35065 7.80644Z" fill="url(#paint13_linear_205_1983)"/>
|
||||
<path d="M14.7895 7.80644L11.9835 5.43546L11.7263 9.80732L15.9875 9.61764L14.7895 7.80644Z" fill="url(#paint14_linear_205_1983)"/>
|
||||
<path d="M6.58707 16.0806L9.15673 14.8315L6.9439 13.1035L6.58707 16.0806Z" fill="#9383FA"/>
|
||||
<path d="M11.9836 14.8315L14.5532 16.0806L14.1964 13.1035L11.9836 14.8315Z" fill="#9383FA"/>
|
||||
<path d="M14.1987 13.1035L14.5556 16.0806L16.5645 13.0735L14.1987 13.1035Z" fill="url(#paint15_linear_205_1983)"/>
|
||||
<path d="M6.94157 13.1035L6.58473 16.0806L4.57581 13.0735L6.94157 13.1035Z" fill="url(#paint16_linear_205_1983)"/>
|
||||
<path d="M15.9875 9.61761L11.7263 9.80729L12.1202 11.9978L12.7505 10.6794L14.2659 11.3687L15.9875 9.61761Z" fill="#9C5ADD"/>
|
||||
<path d="M6.88127 11.3687L8.3897 10.6794L9.01995 11.9978L9.41386 9.80729L5.15271 9.61761L6.88127 11.3687Z" fill="#9C5ADD"/>
|
||||
<path d="M12.1203 11.9979L11.7264 9.80731L11.6267 11.186L11.6198 14.5817L12.1203 11.9979Z" fill="url(#paint17_linear_205_1983)"/>
|
||||
<path d="M9.02002 11.9979L9.52051 14.5817L9.51356 11.186L9.41393 9.80731L9.02002 11.9979Z" fill="url(#paint18_linear_205_1983)"/>
|
||||
<path d="M12.1805 17.2001L12.1898 16.5038L11.9998 16.3373H9.14048L8.95743 16.5038L8.97365 17.2094L6.58472 16.0805L7.41887 16.7629L9.11731 17.938H12.0206L13.7191 16.7629L14.5532 16.0805L12.1805 17.2001Z" fill="#DF7554"/>
|
||||
<path d="M11.9835 14.8315L11.6197 14.5816H9.52046L9.15667 14.8315L8.95972 16.5062L9.14277 16.3396H11.9998L12.1898 16.5062L11.9835 14.8315Z" fill="#161616" stroke="#161616" stroke-width="0.00668686" stroke-miterlimit="10" stroke-linejoin="round"/>
|
||||
<path d="M12.7505 10.6794L12.1202 11.9978L14.2659 11.3687L12.7505 10.6794Z" fill="#161616"/>
|
||||
<path d="M8.38975 10.6794L9.02 11.9978L6.88132 11.3687L8.38975 10.6794Z" fill="#161616"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_205_1983" x1="10.5701" y1="2.32082" x2="10.5701" y2="14.5805" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FB7FE4"/>
|
||||
<stop offset="1" stop-color="#BCABFB"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_205_1983" x1="17.1393" y1="7.80661" x2="17.1393" y2="13.103" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#B65FE5"/>
|
||||
<stop offset="1" stop-color="#ADA2FC"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_205_1983" x1="4.00472" y1="7.80661" x2="4.00472" y2="13.103" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#B65FE5"/>
|
||||
<stop offset="1" stop-color="#ADA2FC"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_205_1983" x1="11.6201" y1="13.0992" x2="14.266" y2="13.0992" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#C8A8F7"/>
|
||||
<stop offset="1" stop-color="#BAAAFB"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_205_1983" x1="6.88185" y1="13.0992" x2="9.52015" y2="13.0992" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#C8A8F7"/>
|
||||
<stop offset="1" stop-color="#BAAAFB"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint5_linear_205_1983" x1="13.3344" y1="7.76991" x2="21.1442" y2="3.25322" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#541758"/>
|
||||
<stop offset="0.4286" stop-color="#4F206C"/>
|
||||
<stop offset="0.62" stop-color="#4D2577"/>
|
||||
<stop offset="1" stop-color="#8B45B6"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint6_linear_205_1983" x1="6.4016" y1="7.72852" x2="-0.325901" y2="2.07384" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#541758"/>
|
||||
<stop offset="0.4286" stop-color="#4F206C"/>
|
||||
<stop offset="0.62" stop-color="#4D2577"/>
|
||||
<stop offset="1" stop-color="#8B45B6"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint7_linear_205_1983" x1="5.15329" y1="11.3603" x2="6.94246" y2="11.3603" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#BA86F3"/>
|
||||
<stop offset="0.5281" stop-color="#B786F4"/>
|
||||
<stop offset="0.8987" stop-color="#AE86F5"/>
|
||||
<stop offset="1" stop-color="#AA86F6"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint8_linear_205_1983" x1="14.1978" y1="11.3603" x2="15.987" y2="11.3603" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#BA86F3"/>
|
||||
<stop offset="0.5281" stop-color="#B786F4"/>
|
||||
<stop offset="0.8987" stop-color="#AE86F5"/>
|
||||
<stop offset="1" stop-color="#AA86F6"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint9_linear_205_1983" x1="17.3174" y1="13.0727" x2="17.3174" y2="17.2628" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#906EF7"/>
|
||||
<stop offset="1" stop-color="#575ADE"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint10_linear_205_1983" x1="3.82656" y1="13.0727" x2="3.82656" y2="17.2628" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#906EF7"/>
|
||||
<stop offset="1" stop-color="#575ADE"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint11_linear_205_1983" x1="1.86302" y1="2.72998" x2="9.1562" y2="2.72998" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#BB65ED"/>
|
||||
<stop offset="1" stop-color="#E560E3"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint12_linear_205_1983" x1="11.984" y1="2.72998" x2="19.2772" y2="2.72998" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#E560E3"/>
|
||||
<stop offset="0.2946" stop-color="#DE61E5"/>
|
||||
<stop offset="0.7098" stop-color="#CC63E9"/>
|
||||
<stop offset="1" stop-color="#BB65ED"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint13_linear_205_1983" x1="7.28362" y1="5.43502" x2="7.28362" y2="9.80699" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#DC69E6"/>
|
||||
<stop offset="1" stop-color="#C289F3"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint14_linear_205_1983" x1="13.8566" y1="5.43502" x2="13.8566" y2="9.80699" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#DC69E6"/>
|
||||
<stop offset="1" stop-color="#C289F3"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint15_linear_205_1983" x1="15.3805" y1="9.23862" x2="15.3805" y2="17.035" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#6848BA"/>
|
||||
<stop offset="0.3363" stop-color="#6356D5"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint16_linear_205_1983" x1="5.75984" y1="13.0726" x2="5.75984" y2="16.0807" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#6848BA"/>
|
||||
<stop offset="0.3363" stop-color="#6356D5"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint17_linear_205_1983" x1="11.8703" y1="14.5805" x2="11.8703" y2="9.80701" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#BA86F3"/>
|
||||
<stop offset="0.5281" stop-color="#B786F4"/>
|
||||
<stop offset="0.8987" stop-color="#AE86F5"/>
|
||||
<stop offset="1" stop-color="#AA86F6"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint18_linear_205_1983" x1="9.01979" y1="12.1938" x2="9.52017" y2="12.1938" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#BA86F3"/>
|
||||
<stop offset="0.5281" stop-color="#B786F4"/>
|
||||
<stop offset="0.8987" stop-color="#AE86F5"/>
|
||||
<stop offset="1" stop-color="#AA86F6"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.7 KiB |
3
chain138-snap/packages/site/src/assets/logo.svg
Normal file
3
chain138-snap/packages/site/src/assets/logo.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="125" height="125" viewBox="0 0 125 125" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M88.5473 0.415527H36.5005C13.893 0.415527 0.415527 13.9484 0.415527 36.5446V88.5034C0.415527 111.1 13.893 124.57 36.5005 124.57H88.4852C111.093 124.57 124.57 111.1 124.57 88.5034V36.5446C124.632 13.9484 111.155 0.415527 88.5473 0.415527ZM59.8533 92.2281C59.8533 94.0904 58.9216 95.7665 57.3068 96.7597C56.4373 97.3184 55.5057 97.5667 54.5119 97.5667C53.7045 97.5667 52.8971 97.3805 52.0897 97.008L30.3518 86.1445C27.2464 84.5305 25.2589 81.3645 25.2589 77.8261V57.2785C25.2589 55.4161 26.1905 53.7401 27.8053 52.7468C29.4201 51.7536 31.3455 51.6915 33.0224 52.4985L54.7604 63.3621C57.9279 64.9761 59.9154 68.142 59.9154 71.6804V92.2281H59.8533ZM58.549 59.0166L35.1962 46.4149C33.5193 45.4838 32.4635 43.6835 32.4635 41.635C32.4635 39.6485 33.5193 37.7862 35.1962 36.855L58.549 24.2533C61.0333 22.9496 63.9524 22.9496 66.4368 24.2533L89.7895 36.855C91.4664 37.7862 92.5223 39.5864 92.5223 41.635C92.5223 43.6835 91.4664 45.4838 89.7895 46.4149L66.4368 59.0166C65.1946 59.6995 63.8282 60.0099 62.4618 60.0099C61.0954 60.0099 59.7912 59.6995 58.549 59.0166ZM99.789 77.8261C99.789 81.3645 97.8015 84.5925 94.634 86.1445L72.896 97.008C72.1507 97.3805 71.3433 97.5667 70.4738 97.5667C69.4801 97.5667 68.5484 97.3184 67.6789 96.7597C66.0641 95.7665 65.1325 94.0904 65.1325 92.2281V71.6804C65.1325 68.142 67.1199 64.914 70.2875 63.3621L92.0254 52.4985C93.7023 51.6915 95.6277 51.7536 97.2425 52.7468C98.8573 53.7401 99.789 55.4161 99.789 57.2785V77.8261Z" fill="#24272A"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
31
chain138-snap/packages/site/src/assets/metamask_fox.svg
Normal file
31
chain138-snap/packages/site/src/assets/metamask_fox.svg
Normal file
@@ -0,0 +1,31 @@
|
||||
<svg width="27" height="26" viewBox="0 0 27 26" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M25.2148 0.942841L15.1025 8.58272L16.983 4.08028L25.2148 0.942841Z" fill="#E17726" stroke="#E17726" stroke-width="0.269643" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M1.88965 0.942841L11.9119 8.65402L10.1215 4.08028L1.88965 0.942841Z" fill="#E27625" stroke="#E27625" stroke-width="0.269643" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M21.5739 18.6572L18.8833 22.854L24.6446 24.4737L26.2949 18.7488L21.5739 18.6572Z" fill="#E27625" stroke="#E27625" stroke-width="0.269643" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M0.819336 18.7488L2.4597 24.4737L8.21097 22.854L5.53038 18.6572L0.819336 18.7488Z" fill="#E27625" stroke="#E27625" stroke-width="0.269643" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7.90089 11.5674L6.30054 14.0325L12.0018 14.2974L11.8118 8.03265L7.90089 11.5674Z" fill="#E27625" stroke="#E27625" stroke-width="0.269643" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M19.2034 11.5674L15.2326 7.96133L15.1025 14.2973L20.8038 14.0325L19.2034 11.5674Z" fill="#E27625" stroke="#E27625" stroke-width="0.269643" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8.21094 22.854L11.6617 21.1529L8.69104 18.7896L8.21094 22.854Z" fill="#E27625" stroke="#E27625" stroke-width="0.269643" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M15.4426 21.1529L18.8834 22.854L18.4133 18.7896L15.4426 21.1529Z" fill="#E27625" stroke="#E27625" stroke-width="0.269643" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M18.8834 22.8541L15.4426 21.1529L15.7227 23.4347L15.6927 24.4024L18.8834 22.8541Z" fill="#D5BFB2" stroke="#D5BFB2" stroke-width="0.269643" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8.21094 22.8541L11.4116 24.4024L11.3916 23.4347L11.6617 21.1529L8.21094 22.8541Z" fill="#D5BFB2" stroke="#D5BFB2" stroke-width="0.269643" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M11.4717 17.282L8.61108 16.4263L10.6315 15.479L11.4717 17.282Z" fill="#233447" stroke="#233447" stroke-width="0.269643" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M15.6326 17.282L16.4728 15.479L18.5032 16.4263L15.6326 17.282Z" fill="#233447" stroke="#233447" stroke-width="0.269643" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8.21111 22.854L8.71122 18.6572L5.53052 18.7488L8.21111 22.854Z" fill="#CC6228" stroke="#CC6228" stroke-width="0.269643" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M18.3933 18.6572L18.8834 22.854L21.574 18.7488L18.3933 18.6572Z" fill="#CC6228" stroke="#CC6228" stroke-width="0.269643" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M20.8038 14.0325L15.1025 14.2974L15.6327 17.282L16.4728 15.479L18.5033 16.4264L20.8038 14.0325Z" fill="#CC6228" stroke="#CC6228" stroke-width="0.269643" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8.61117 16.4264L10.6316 15.479L11.4718 17.282L12.0019 14.2974L6.30066 14.0325L8.61117 16.4264Z" fill="#CC6228" stroke="#CC6228" stroke-width="0.269643" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6.30066 14.0325L8.69119 18.7896L8.61117 16.4264L6.30066 14.0325Z" fill="#E27525" stroke="#E27525" stroke-width="0.269643" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M18.5035 16.4264L18.4135 18.7896L20.804 14.0325L18.5035 16.4264Z" fill="#E27525" stroke="#E27525" stroke-width="0.269643" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12.0019 14.2974L11.4718 17.282L12.1419 20.8066L12.292 16.1615L12.0019 14.2974Z" fill="#E27525" stroke="#E27525" stroke-width="0.269643" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M15.1027 14.2974L14.8226 16.1513L14.9627 20.8066L15.6328 17.282L15.1027 14.2974Z" fill="#E27525" stroke="#E27525" stroke-width="0.269643" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M15.6327 17.282L14.9625 20.8065L15.4426 21.1529L18.4133 18.7896L18.5033 16.4263L15.6327 17.282Z" fill="#F5841F" stroke="#F5841F" stroke-width="0.269643" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8.61108 16.4263L8.6911 18.7896L11.6618 21.1529L12.1419 20.8065L11.4717 17.282L8.61108 16.4263Z" fill="#F5841F" stroke="#F5841F" stroke-width="0.269643" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M15.6926 24.4024L15.7226 23.4346L15.4625 23.2105H11.6417L11.3916 23.4346L11.4116 24.4024L8.21094 22.854L9.33118 23.7912L11.6017 25.3904H15.4925L17.773 23.7912L18.8833 22.854L15.6926 24.4024Z" fill="#C0AC9D" stroke="#C0AC9D" stroke-width="0.269643" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M15.4426 21.1529L14.9625 20.8065H12.1419L11.6618 21.1529L11.3917 23.4347L11.6418 23.2106H15.4626L15.7227 23.4347L15.4426 21.1529Z" fill="#161616" stroke="#161616" stroke-width="0.269643" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M25.6449 9.08186L26.4951 4.86464L25.2148 0.942841L15.4426 8.32806L19.2035 11.5674L24.5146 13.1463L25.6849 11.7507L25.1748 11.3738L25.985 10.62L25.3648 10.1311L26.175 9.4995L25.6449 9.08186Z" fill="#763E1A" stroke="#763E1A" stroke-width="0.269643" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M0.609375 4.86464L1.46957 9.08186L0.919443 9.4995L1.73962 10.1311L1.11949 10.62L1.92967 11.3738L1.41955 11.7507L2.58981 13.1463L7.90099 11.5674L11.6618 8.32806L1.88966 0.942841L0.609375 4.86464Z" fill="#763E1A" stroke="#763E1A" stroke-width="0.269643" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M24.5147 13.1463L19.2035 11.5674L20.8039 14.0325L18.4133 18.7896L21.574 18.7488H26.2951L24.5147 13.1463Z" fill="#F5841F" stroke="#F5841F" stroke-width="0.269643" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7.9009 11.5674L2.58973 13.1463L0.819336 18.7488H5.53038L8.69107 18.7896L6.30055 14.0325L7.9009 11.5674Z" fill="#F5841F" stroke="#F5841F" stroke-width="0.269643" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M15.1024 14.2974L15.4425 8.32806L16.9829 4.08029H10.1213L11.6617 8.32806L12.0018 14.2974L12.1318 16.1717L12.1418 20.8065H14.9624L14.9724 16.1717L15.1024 14.2974Z" fill="#F5841F" stroke="#F5841F" stroke-width="0.269643" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.1 KiB |
124
chain138-snap/packages/site/src/components/Buttons.tsx
Normal file
124
chain138-snap/packages/site/src/components/Buttons.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import type { ComponentProps } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { ReactComponent as FlaskFox } from '../assets/flask_fox.svg';
|
||||
import { useMetaMask, useRequestSnap } from '../hooks';
|
||||
import { shouldDisplayReconnectButton } from '../utils';
|
||||
|
||||
const Link = styled.a`
|
||||
display: flex;
|
||||
align-self: flex-start;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: ${(props) => props.theme.fontSizes.small};
|
||||
border-radius: ${(props) => props.theme.radii.button};
|
||||
border: 1px solid ${(props) => props.theme.colors.background?.inverse};
|
||||
background-color: ${(props) => props.theme.colors.background?.inverse};
|
||||
color: ${(props) => props.theme.colors.text?.inverse};
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
padding: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
border: 1px solid ${(props) => props.theme.colors.background?.inverse};
|
||||
color: ${(props) => props.theme.colors.text?.default};
|
||||
}
|
||||
|
||||
${({ theme }) => theme.mediaQueries.small} {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Button = styled.button`
|
||||
display: flex;
|
||||
align-self: flex-start;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: auto;
|
||||
${({ theme }) => theme.mediaQueries.small} {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
const ButtonText = styled.span`
|
||||
margin-left: 1rem;
|
||||
`;
|
||||
|
||||
const ConnectedContainer = styled.div`
|
||||
display: flex;
|
||||
align-self: flex-start;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: ${(props) => props.theme.fontSizes.small};
|
||||
border-radius: ${(props) => props.theme.radii.button};
|
||||
border: 1px solid ${(props) => props.theme.colors.background?.inverse};
|
||||
background-color: ${(props) => props.theme.colors.background?.inverse};
|
||||
color: ${(props) => props.theme.colors.text?.inverse};
|
||||
font-weight: bold;
|
||||
padding: 1.2rem;
|
||||
`;
|
||||
|
||||
const ConnectedIndicator = styled.div`
|
||||
content: ' ';
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background-color: green;
|
||||
`;
|
||||
|
||||
export const InstallFlaskButton = () => (
|
||||
<Link href="https://metamask.io/flask/" target="_blank">
|
||||
<FlaskFox />
|
||||
<ButtonText>Install MetaMask Flask</ButtonText>
|
||||
</Link>
|
||||
);
|
||||
|
||||
export const ConnectButton = (props: ComponentProps<typeof Button>) => {
|
||||
return (
|
||||
<Button {...props}>
|
||||
<FlaskFox />
|
||||
<ButtonText>Connect</ButtonText>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export const ReconnectButton = (props: ComponentProps<typeof Button>) => {
|
||||
return (
|
||||
<Button {...props}>
|
||||
<FlaskFox />
|
||||
<ButtonText>Reconnect</ButtonText>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export const SendHelloButton = (props: ComponentProps<typeof Button>) => {
|
||||
return <Button {...props}>Send message</Button>;
|
||||
};
|
||||
|
||||
export const HeaderButtons = () => {
|
||||
const requestSnap = useRequestSnap();
|
||||
const { isFlask, installedSnap } = useMetaMask();
|
||||
|
||||
if (!isFlask && !installedSnap) {
|
||||
return <InstallFlaskButton />;
|
||||
}
|
||||
|
||||
if (!installedSnap) {
|
||||
return <ConnectButton onClick={requestSnap} />;
|
||||
}
|
||||
|
||||
if (shouldDisplayReconnectButton(installedSnap)) {
|
||||
return <ReconnectButton onClick={requestSnap} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ConnectedContainer>
|
||||
<ConnectedIndicator />
|
||||
<ButtonText>Connected</ButtonText>
|
||||
</ConnectedContainer>
|
||||
);
|
||||
};
|
||||
60
chain138-snap/packages/site/src/components/Card.tsx
Normal file
60
chain138-snap/packages/site/src/components/Card.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
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;
|
||||
flex-direction: column;
|
||||
width: ${({ fullWidth }) => (fullWidth ? '100%' : '250px')};
|
||||
background-color: ${({ theme }) => theme.colors.card?.default};
|
||||
margin-top: 2.4rem;
|
||||
margin-bottom: 2.4rem;
|
||||
padding: 2.4rem;
|
||||
border: 1px solid ${({ theme }) => theme.colors.border?.default};
|
||||
border-radius: ${({ theme }) => theme.radii.default};
|
||||
box-shadow: ${({ theme }) => theme.shadows.default};
|
||||
filter: opacity(${({ disabled }) => (disabled ? '.4' : '1')});
|
||||
align-self: stretch;
|
||||
${({ theme }) => theme.mediaQueries.small} {
|
||||
width: 100%;
|
||||
margin-top: 1.2rem;
|
||||
margin-bottom: 1.2rem;
|
||||
padding: 1.6rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const Title = styled.h2`
|
||||
font-size: ${({ theme }) => theme.fontSizes.large};
|
||||
margin: 0;
|
||||
${({ theme }) => theme.mediaQueries.small} {
|
||||
font-size: ${({ theme }) => theme.fontSizes.text};
|
||||
}
|
||||
`;
|
||||
|
||||
const Description = styled.div`
|
||||
margin-top: 2.4rem;
|
||||
margin-bottom: 2.4rem;
|
||||
`;
|
||||
|
||||
export const Card = ({ content, disabled = false, fullWidth }: CardProps) => {
|
||||
const { title, description, button } = content;
|
||||
return (
|
||||
<CardWrapper fullWidth={fullWidth} disabled={disabled}>
|
||||
{title && <Title>{title}</Title>}
|
||||
<Description>{description}</Description>
|
||||
{button}
|
||||
</CardWrapper>
|
||||
);
|
||||
};
|
||||
60
chain138-snap/packages/site/src/components/Footer.tsx
Normal file
60
chain138-snap/packages/site/src/components/Footer.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import styled, { useTheme } from 'styled-components';
|
||||
|
||||
import { getBuildVersion } from '../config';
|
||||
import { MetaMask } from './MetaMask';
|
||||
import { PoweredBy } from './PoweredBy';
|
||||
import { ReactComponent as MetaMaskFox } from '../assets/metamask_fox.svg';
|
||||
|
||||
const FooterWrapper = styled.footer`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
padding-top: 2.4rem;
|
||||
padding-bottom: 2.4rem;
|
||||
border-top: 1px solid ${(props) => props.theme.colors.border?.default};
|
||||
`;
|
||||
|
||||
const VersionSpan = styled.span`
|
||||
font-size: ${({ theme }) => theme.fontSizes.small};
|
||||
color: ${(props) => props.theme.colors.text?.muted};
|
||||
`;
|
||||
|
||||
const PoweredByButton = styled.a`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1.2rem;
|
||||
border-radius: ${({ theme }) => theme.radii.button};
|
||||
box-shadow: ${({ theme }) => theme.shadows.button};
|
||||
background-color: ${({ theme }) => theme.colors.background?.alternative};
|
||||
`;
|
||||
|
||||
const PoweredByContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 1rem;
|
||||
`;
|
||||
|
||||
export const Footer = () => {
|
||||
const theme = useTheme();
|
||||
const buildVersion = getBuildVersion();
|
||||
|
||||
return (
|
||||
<FooterWrapper>
|
||||
<PoweredByButton href="https://docs.metamask.io/" target="_blank">
|
||||
<MetaMaskFox />
|
||||
<PoweredByContainer>
|
||||
<PoweredBy color={theme.colors.text?.muted} />
|
||||
<MetaMask color={theme.colors.text?.default} />
|
||||
</PoweredByContainer>
|
||||
</PoweredByButton>
|
||||
{buildVersion ? (
|
||||
<VersionSpan title="Build version">v{buildVersion}</VersionSpan>
|
||||
) : null}
|
||||
</FooterWrapper>
|
||||
);
|
||||
};
|
||||
61
chain138-snap/packages/site/src/components/Header.tsx
Normal file
61
chain138-snap/packages/site/src/components/Header.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import styled, { useTheme } from 'styled-components';
|
||||
|
||||
import { HeaderButtons } from './Buttons';
|
||||
import { SnapLogo } from './SnapLogo';
|
||||
import { Toggle } from './Toggle';
|
||||
import { getThemePreference } from '../utils';
|
||||
|
||||
const HeaderWrapper = styled.header`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 2.4rem;
|
||||
border-bottom: 1px solid ${(props) => props.theme.colors.border?.default};
|
||||
`;
|
||||
|
||||
const Title = styled.p`
|
||||
font-size: ${(props) => props.theme.fontSizes.title};
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
margin-left: 1.2rem;
|
||||
${({ theme }) => theme.mediaQueries.small} {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const LogoWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const RightContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
export const Header = ({
|
||||
handleToggleClick,
|
||||
}: {
|
||||
handleToggleClick: () => void;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<HeaderWrapper>
|
||||
<LogoWrapper>
|
||||
<SnapLogo color={theme.colors.icon?.default} size={36} />
|
||||
<Title>template-snap</Title>
|
||||
</LogoWrapper>
|
||||
<RightContainer>
|
||||
<Toggle
|
||||
onToggle={handleToggleClick}
|
||||
defaultChecked={getThemePreference()}
|
||||
/>
|
||||
<HeaderButtons />
|
||||
</RightContainer>
|
||||
</HeaderWrapper>
|
||||
);
|
||||
};
|
||||
42
chain138-snap/packages/site/src/components/MetaMask.tsx
Normal file
42
chain138-snap/packages/site/src/components/MetaMask.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
export const MetaMask = ({ color }: { color?: string | undefined }) => (
|
||||
<svg
|
||||
width="98"
|
||||
height="12"
|
||||
viewBox="0 0 98 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M81.8298 5.64075C81.1373 5.22444 80.3734 4.92839 79.6503 4.55833C79.1818 4.3178 78.6828 4.10501 78.2754 3.79971C77.5828 3.28163 77.7254 2.26397 78.4485 1.8199C79.4874 1.1908 81.2086 1.54236 81.3919 2.82831C81.3919 2.85607 81.4224 2.87457 81.453 2.87457H83.0214C83.0622 2.87457 83.0927 2.84681 83.0825 2.80981C83.0011 1.92167 82.6242 1.18155 81.9317 0.709728C81.2697 0.256407 80.516 0.0158691 79.7114 0.0158691C75.5663 0.0158691 75.1894 4.00325 77.4199 5.26144C77.6745 5.40947 79.8642 6.40862 80.6382 6.84344C81.4123 7.27826 81.6567 8.07389 81.3206 8.70298C81.015 9.27658 80.2206 9.67439 79.4262 9.62813C78.5605 9.58187 77.8884 9.15631 77.6541 8.4902C77.6134 8.36993 77.593 8.13865 77.593 8.03688C77.593 8.00913 77.5625 7.98137 77.5319 7.98137H75.8311C75.8005 7.98137 75.77 8.00913 75.77 8.03688C75.77 9.15631 76.0755 9.77615 76.9106 10.3405C77.6949 10.8771 78.5504 11.0991 79.4364 11.0991C81.7585 11.0991 82.9603 9.90567 83.2048 8.66598C83.4186 7.45404 83.0214 6.36237 81.8298 5.64075Z"
|
||||
fill={color}
|
||||
/>
|
||||
<path
|
||||
d="M7.98096 0.219391H7.2273H6.40234C6.37179 0.219391 6.35142 0.237894 6.34123 0.256397L4.94594 4.43805C4.92557 4.49356 4.84409 4.49356 4.82372 4.43805L3.42842 0.256397C3.41824 0.228642 3.39787 0.219391 3.36732 0.219391H2.54236H1.7887H0.770236C0.739682 0.219391 0.709127 0.247145 0.709127 0.2749V10.9511C0.709127 10.9788 0.739682 11.0066 0.770236 11.0066H2.47107C2.50162 11.0066 2.53218 10.9788 2.53218 10.9511V2.83755C2.53218 2.77279 2.63402 2.76354 2.65439 2.81905L4.05987 7.02846L4.16172 7.32451C4.1719 7.35226 4.19227 7.36152 4.22283 7.36152H5.52646C5.55701 7.36152 5.57738 7.34301 5.58757 7.32451L5.68941 7.02846L7.09489 2.81905C7.11526 2.75429 7.21711 2.77279 7.21711 2.83755V10.9511C7.21711 10.9788 7.24766 11.0066 7.27822 11.0066H8.97905C9.00961 11.0066 9.04016 10.9788 9.04016 10.9511V0.2749C9.04016 0.247145 9.00961 0.219391 8.97905 0.219391H7.98096Z"
|
||||
fill={color}
|
||||
/>
|
||||
<path
|
||||
d="M55.7876 0.219391C55.7571 0.219391 55.7367 0.237894 55.7265 0.256397L54.3312 4.43805C54.3109 4.49356 54.2294 4.49356 54.209 4.43805L52.8137 0.256397C52.8035 0.228642 52.7832 0.219391 52.7526 0.219391H50.1657C50.1352 0.219391 50.1046 0.247145 50.1046 0.2749V10.9511C50.1046 10.9788 50.1352 11.0066 50.1657 11.0066H51.8666C51.8971 11.0066 51.9277 10.9788 51.9277 10.9511V2.83755C51.9277 2.77279 52.0295 2.76354 52.0499 2.81905L53.4554 7.02846L53.5572 7.32451C53.5674 7.35226 53.5878 7.36152 53.6183 7.36152H54.922C54.9525 7.36152 54.9729 7.34301 54.9831 7.32451L55.0849 7.02846L56.4904 2.81905C56.5108 2.75429 56.6126 2.77279 56.6126 2.83755V10.9511C56.6126 10.9788 56.6432 11.0066 56.6737 11.0066H58.3745C58.4051 11.0066 58.4357 10.9788 58.4357 10.9511V0.2749C58.4357 0.247145 58.4051 0.219391 58.3745 0.219391H55.7876Z"
|
||||
fill={color}
|
||||
/>
|
||||
<path
|
||||
d="M33.8499 0.219391H30.6825H28.9817H25.8142C25.7837 0.219391 25.7531 0.247145 25.7531 0.2749V1.60711C25.7531 1.63486 25.7837 1.66262 25.8142 1.66262H28.9206V10.9511C28.9206 10.9788 28.9511 11.0066 28.9817 11.0066H30.6825C30.7131 11.0066 30.7436 10.9788 30.7436 10.9511V1.66262H33.8499C33.8805 1.66262 33.911 1.63486 33.911 1.60711V0.2749C33.911 0.247145 33.8907 0.219391 33.8499 0.219391Z"
|
||||
fill={color}
|
||||
/>
|
||||
<path
|
||||
d="M43.8819 11.0066H45.4299C45.4707 11.0066 45.5012 10.9696 45.491 10.9326L42.2931 0.219409C42.2829 0.191655 42.2625 0.182404 42.2319 0.182404H41.6412H40.6024H40.0117C39.9811 0.182404 39.9608 0.200906 39.9506 0.219409L36.7526 10.9326C36.7424 10.9696 36.773 11.0066 36.8137 11.0066H38.3618C38.3923 11.0066 38.4127 10.9881 38.4229 10.9696L39.3497 7.85186C39.3599 7.82411 39.3802 7.81485 39.4108 7.81485H42.8328C42.8634 7.81485 42.8838 7.83336 42.8939 7.85186L43.8208 10.9696C43.8309 10.9881 43.8615 11.0066 43.8819 11.0066ZM39.8182 6.28836L41.0607 2.11596C41.0811 2.06045 41.1626 2.06045 41.1829 2.11596L42.4255 6.28836C42.4356 6.32537 42.4051 6.36238 42.3643 6.36238H39.8793C39.8386 6.36238 39.808 6.32537 39.8182 6.28836Z"
|
||||
fill={color}
|
||||
/>
|
||||
<path
|
||||
d="M70.2805 11.0066H71.8286C71.8693 11.0066 71.8999 10.9696 71.8897 10.9326L68.6917 0.219409C68.6815 0.191655 68.6611 0.182404 68.6306 0.182404H68.0399H67.001H66.4103C66.3798 0.182404 66.3594 0.200906 66.3492 0.219409L63.1512 10.9326C63.1411 10.9696 63.1716 11.0066 63.2123 11.0066H64.7604C64.791 11.0066 64.8113 10.9881 64.8215 10.9696L65.7483 7.85186C65.7585 7.82411 65.7789 7.81485 65.8094 7.81485H69.2315C69.262 7.81485 69.2824 7.83336 69.2926 7.85186L70.2194 10.9696C70.2296 10.9881 70.2499 11.0066 70.2805 11.0066ZM66.2168 6.28836L67.4594 2.11596C67.4797 2.06045 67.5612 2.06045 67.5816 2.11596L68.8241 6.28836C68.8343 6.32537 68.8037 6.36238 68.763 6.36238H66.2779C66.2372 6.36238 66.2066 6.32537 66.2168 6.28836Z"
|
||||
fill={color}
|
||||
/>
|
||||
<path
|
||||
d="M15.9453 9.42455V6.11253C15.9453 6.08478 15.9759 6.05702 16.0064 6.05702H20.5386C20.5692 6.05702 20.5997 6.02927 20.5997 6.00151V4.6693C20.5997 4.64155 20.5692 4.6138 20.5386 4.6138H16.0064C15.9759 4.6138 15.9453 4.58604 15.9453 4.55829V1.72734C15.9453 1.69959 15.9759 1.67183 16.0064 1.67183H21.1599C21.1904 1.67183 21.221 1.64408 21.221 1.61633V0.284116C21.221 0.256361 21.1904 0.228607 21.1599 0.228607H15.9453H14.1834C14.1528 0.228607 14.1223 0.256361 14.1223 0.284116V1.67183V4.62305V6.06628V9.48931V10.951C14.1223 10.9788 14.1528 11.0066 14.1834 11.0066H15.9453H21.3738C21.4043 11.0066 21.4349 10.9788 21.4349 10.951V9.54482C21.4349 9.51707 21.4043 9.48931 21.3738 9.48931H15.9963C15.9657 9.48006 15.9453 9.46156 15.9453 9.42455Z"
|
||||
fill={color}
|
||||
/>
|
||||
<path
|
||||
d="M97.3716 10.914L91.4849 5.39092C91.4645 5.37242 91.4645 5.33541 91.4849 5.31691L96.7809 0.321122C96.8216 0.284116 96.7911 0.228607 96.7402 0.228607H94.5708C94.5505 0.228607 94.5403 0.237859 94.5301 0.24711L90.0387 4.48428C89.9979 4.52128 89.9368 4.49353 89.9368 4.44727V0.284116C89.9368 0.256361 89.9063 0.228607 89.8757 0.228607H88.1749C88.1443 0.228607 88.1138 0.256361 88.1138 0.284116V10.9603C88.1138 10.9881 88.1443 11.0158 88.1749 11.0158H89.8757C89.9063 11.0158 89.9368 10.9881 89.9368 10.9603V6.26056C89.9368 6.2143 90.0081 6.18655 90.0387 6.22355L95.131 10.9973C95.1412 11.0066 95.1615 11.0158 95.1717 11.0158H97.3411C97.3818 11.0066 97.4124 10.9418 97.3716 10.914Z"
|
||||
fill={color}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
14
chain138-snap/packages/site/src/components/PoweredBy.tsx
Normal file
14
chain138-snap/packages/site/src/components/PoweredBy.tsx
Normal file
File diff suppressed because one or more lines are too long
20
chain138-snap/packages/site/src/components/SnapLogo.tsx
Normal file
20
chain138-snap/packages/site/src/components/SnapLogo.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
export const SnapLogo = ({
|
||||
color,
|
||||
size,
|
||||
}: {
|
||||
color?: string | undefined;
|
||||
size: number;
|
||||
}) => (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 125 125`}
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M88.5473 0.415527H36.5005C13.893 0.415527 0.415527 13.9484 0.415527 36.5446V88.5034C0.415527 111.1 13.893 124.57 36.5005 124.57H88.4852C111.093 124.57 124.57 111.1 124.57 88.5034V36.5446C124.632 13.9484 111.155 0.415527 88.5473 0.415527ZM59.8533 92.2281C59.8533 94.0904 58.9216 95.7665 57.3068 96.7597C56.4373 97.3184 55.5057 97.5667 54.5119 97.5667C53.7045 97.5667 52.8971 97.3805 52.0897 97.008L30.3518 86.1445C27.2464 84.5305 25.2589 81.3645 25.2589 77.8261V57.2785C25.2589 55.4161 26.1905 53.7401 27.8053 52.7468C29.4201 51.7536 31.3455 51.6915 33.0224 52.4985L54.7604 63.3621C57.9279 64.9761 59.9154 68.142 59.9154 71.6804V92.2281H59.8533ZM58.549 59.0166L35.1962 46.4149C33.5193 45.4838 32.4635 43.6835 32.4635 41.635C32.4635 39.6485 33.5193 37.7862 35.1962 36.855L58.549 24.2533C61.0333 22.9496 63.9524 22.9496 66.4368 24.2533L89.7895 36.855C91.4664 37.7862 92.5223 39.5864 92.5223 41.635C92.5223 43.6835 91.4664 45.4838 89.7895 46.4149L66.4368 59.0166C65.1946 59.6995 63.8282 60.0099 62.4618 60.0099C61.0954 60.0099 59.7912 59.6995 58.549 59.0166ZM99.789 77.8261C99.789 81.3645 97.8015 84.5925 94.634 86.1445L72.896 97.008C72.1507 97.3805 71.3433 97.5667 70.4738 97.5667C69.4801 97.5667 68.5484 97.3184 67.6789 96.7597C66.0641 95.7665 65.1325 94.0904 65.1325 92.2281V71.6804C65.1325 68.142 67.1199 64.914 70.2875 63.3621L92.0254 52.4985C93.7023 51.6915 95.6277 51.7536 97.2425 52.7468C98.8573 53.7401 99.789 55.4161 99.789 57.2785V77.8261Z"
|
||||
fill={color}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
121
chain138-snap/packages/site/src/components/Toggle.tsx
Normal file
121
chain138-snap/packages/site/src/components/Toggle.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import { useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
type CheckedProps = {
|
||||
readonly checked: boolean;
|
||||
};
|
||||
|
||||
const ToggleWrapper = styled.div`
|
||||
touch-action: pan-x;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
margin-right: 2.4rem;
|
||||
${({ theme }) => theme.mediaQueries.small} {
|
||||
margin-right: 2.4rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const ToggleInput = styled.input`
|
||||
border: 0;
|
||||
clip: rect(0 0 0 0);
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
`;
|
||||
|
||||
const IconContainer = styled.div`
|
||||
position: absolute;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
line-height: 0;
|
||||
opacity: 0;
|
||||
transition: opacity 0.25s ease;
|
||||
& > * {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 22px;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
width: 22px;
|
||||
}
|
||||
`;
|
||||
|
||||
const CheckedContainer = styled(IconContainer)<CheckedProps>`
|
||||
opacity: ${({ checked }) => (checked ? 1 : 0)};
|
||||
left: 10px;
|
||||
`;
|
||||
|
||||
const UncheckedContainer = styled(IconContainer)<CheckedProps>`
|
||||
opacity: ${({ checked }) => (checked ? 0 : 1)};
|
||||
right: 10px;
|
||||
`;
|
||||
|
||||
const ToggleContainer = styled.div`
|
||||
width: 68px;
|
||||
height: 36px;
|
||||
padding: 0;
|
||||
border-radius: 36px;
|
||||
background-color: ${({ theme }) => theme.colors.background?.alternative};
|
||||
transition: all 0.2s ease;
|
||||
`;
|
||||
|
||||
const ToggleCircle = styled.div<CheckedProps>`
|
||||
transition: all 0.5s cubic-bezier(0.23, 1, 0.32, 1) 0ms;
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: ${({ checked }) => (checked ? '36px' : '4px')};
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.14);
|
||||
border-radius: 50%;
|
||||
background-color: #ffffff;
|
||||
box-sizing: border-box;
|
||||
transition: all 0.25s ease;
|
||||
`;
|
||||
|
||||
export const Toggle = ({
|
||||
onToggle,
|
||||
defaultChecked = false,
|
||||
}: {
|
||||
onToggle: () => void;
|
||||
defaultChecked?: boolean;
|
||||
}) => {
|
||||
const [checked, setChecked] = useState(defaultChecked);
|
||||
|
||||
const handleChange = () => {
|
||||
onToggle();
|
||||
setChecked(!checked);
|
||||
};
|
||||
|
||||
return (
|
||||
<ToggleWrapper onClick={handleChange}>
|
||||
<ToggleContainer>
|
||||
<CheckedContainer checked={checked}>
|
||||
<span>🌞</span>
|
||||
</CheckedContainer>
|
||||
<UncheckedContainer checked={checked}>
|
||||
<span>🌜</span>
|
||||
</UncheckedContainer>
|
||||
</ToggleContainer>
|
||||
<ToggleCircle checked={checked} />
|
||||
<ToggleInput type="checkbox" aria-label="Toggle Button" />
|
||||
</ToggleWrapper>
|
||||
);
|
||||
};
|
||||
8
chain138-snap/packages/site/src/components/index.ts
Normal file
8
chain138-snap/packages/site/src/components/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export * from './Buttons';
|
||||
export * from './Card';
|
||||
export * from './Footer';
|
||||
export * from './Header';
|
||||
export * from './MetaMask';
|
||||
export * from './PoweredBy';
|
||||
export * from './SnapLogo';
|
||||
export * from './Toggle';
|
||||
16
chain138-snap/packages/site/src/config/index.ts
Normal file
16
chain138-snap/packages/site/src/config/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export { defaultSnapOrigin } from './snap';
|
||||
|
||||
/**
|
||||
* Token-aggregation (or market) API base URL for Snap RPCs.
|
||||
* 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) ||
|
||||
'';
|
||||
|
||||
/** Build ID or git SHA for support/debug (set at build: GATSBY_BUILD_SHA, GATSBY_APP_VERSION). */
|
||||
export const getBuildVersion = (): string =>
|
||||
(typeof process !== 'undefined' &&
|
||||
(process.env?.GATSBY_APP_VERSION || process.env?.GATSBY_BUILD_SHA)) ||
|
||||
'';
|
||||
11
chain138-snap/packages/site/src/config/snap.ts
Normal file
11
chain138-snap/packages/site/src/config/snap.ts
Normal file
@@ -0,0 +1,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.
|
||||
*/
|
||||
export const defaultSnapOrigin =
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
process.env.SNAP_ORIGIN ?? `local:http://localhost:8080`;
|
||||
187
chain138-snap/packages/site/src/config/theme.ts
Normal file
187
chain138-snap/packages/site/src/config/theme.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import type { DefaultTheme } from 'styled-components';
|
||||
import { createGlobalStyle } from 'styled-components';
|
||||
|
||||
const breakpoints = ['600px', '768px', '992px'];
|
||||
|
||||
/**
|
||||
* Common theme properties.
|
||||
*/
|
||||
const theme = {
|
||||
fonts: {
|
||||
default:
|
||||
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif',
|
||||
code: 'ui-monospace,Menlo,Monaco,"Cascadia Mono","Segoe UI Mono","Roboto Mono","Oxygen Mono","Ubuntu Monospace","Source Code Pro","Fira Mono","Droid Sans Mono","Courier New", monospace',
|
||||
},
|
||||
fontSizes: {
|
||||
heading: '5.2rem',
|
||||
mobileHeading: '3.6rem',
|
||||
title: '2.4rem',
|
||||
large: '2rem',
|
||||
text: '1.6rem',
|
||||
small: '1.4rem',
|
||||
},
|
||||
radii: {
|
||||
default: '24px',
|
||||
button: '8px',
|
||||
},
|
||||
breakpoints,
|
||||
mediaQueries: {
|
||||
small: `@media screen and (max-width: ${breakpoints[0] as string})`,
|
||||
medium: `@media screen and (min-width: ${breakpoints[1] as string})`,
|
||||
large: `@media screen and (min-width: ${breakpoints[2] as string})`,
|
||||
},
|
||||
shadows: {
|
||||
default: '0px 7px 42px rgba(0, 0, 0, 0.1)',
|
||||
button: '0px 0px 16.1786px rgba(0, 0, 0, 0.15);',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Light theme color properties.
|
||||
*/
|
||||
export const light: DefaultTheme = {
|
||||
colors: {
|
||||
background: {
|
||||
default: '#FFFFFF',
|
||||
alternative: '#F2F4F6',
|
||||
inverse: '#141618',
|
||||
},
|
||||
icon: {
|
||||
default: '#141618',
|
||||
alternative: '#BBC0C5',
|
||||
},
|
||||
text: {
|
||||
default: '#24272A',
|
||||
muted: '#6A737D',
|
||||
alternative: '#535A61',
|
||||
inverse: '#FFFFFF',
|
||||
},
|
||||
border: {
|
||||
default: '#BBC0C5',
|
||||
},
|
||||
primary: {
|
||||
default: '#6F4CFF',
|
||||
inverse: '#FFFFFF',
|
||||
},
|
||||
card: {
|
||||
default: '#FFFFFF',
|
||||
},
|
||||
error: {
|
||||
default: '#d73a49',
|
||||
alternative: '#b92534',
|
||||
muted: '#d73a4919',
|
||||
},
|
||||
},
|
||||
...theme,
|
||||
};
|
||||
|
||||
/**
|
||||
* Dark theme color properties
|
||||
*/
|
||||
export const dark: DefaultTheme = {
|
||||
colors: {
|
||||
background: {
|
||||
default: '#24272A',
|
||||
alternative: '#141618',
|
||||
inverse: '#FFFFFF',
|
||||
},
|
||||
icon: {
|
||||
default: '#FFFFFF',
|
||||
alternative: '#BBC0C5',
|
||||
},
|
||||
text: {
|
||||
default: '#FFFFFF',
|
||||
muted: '#FFFFFF',
|
||||
alternative: '#D6D9DC',
|
||||
inverse: '#24272A',
|
||||
},
|
||||
border: {
|
||||
default: '#848C96',
|
||||
},
|
||||
primary: {
|
||||
default: '#6F4CFF',
|
||||
inverse: '#FFFFFF',
|
||||
},
|
||||
card: {
|
||||
default: '#141618',
|
||||
},
|
||||
error: {
|
||||
default: '#d73a49',
|
||||
alternative: '#b92534',
|
||||
muted: '#d73a4919',
|
||||
},
|
||||
},
|
||||
...theme,
|
||||
};
|
||||
|
||||
/**
|
||||
* Default style applied to the app.
|
||||
*
|
||||
* @param props - Styled Components props.
|
||||
* @returns Global style React component.
|
||||
*/
|
||||
export const GlobalStyle = createGlobalStyle`
|
||||
html {
|
||||
/* 62.5% of the base size of 16px = 10px.*/
|
||||
font-size: 62.5%;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: ${(props) => props.theme.colors.background?.default};
|
||||
color: ${(props) => props.theme.colors.text?.default};
|
||||
font-family: ${(props) => props.theme.fonts.default};
|
||||
font-size: ${(props) => props.theme.fontSizes.text};
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
* {
|
||||
transition: background-color .1s linear;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-size: ${(props) => props.theme.fontSizes.heading};
|
||||
${(props) => props.theme.mediaQueries.small} {
|
||||
font-size: ${(props) => props.theme.fontSizes.mobileHeading};
|
||||
}
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: ${(props) => props.theme.colors.background?.alternative};
|
||||
font-family: ${(props) => props.theme.fonts.code};
|
||||
padding: 1.2rem;
|
||||
font-weight: normal;
|
||||
font-size: ${(props) => props.theme.fontSizes.text};
|
||||
}
|
||||
|
||||
button {
|
||||
font-size: ${(props) => props.theme.fontSizes.small};
|
||||
border-radius: ${(props) => props.theme.radii.button};
|
||||
background-color: ${(props) => props.theme.colors.background?.inverse};
|
||||
color: ${(props) => props.theme.colors.text?.inverse};
|
||||
border: 1px solid ${(props) => props.theme.colors.background?.inverse};
|
||||
font-weight: bold;
|
||||
padding: 1rem;
|
||||
min-height: 4.2rem;
|
||||
cursor: pointer;
|
||||
transition: all .2s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
border: 1px solid ${(props) => props.theme.colors.background?.inverse};
|
||||
color: ${(props) => props.theme.colors.text?.default};
|
||||
}
|
||||
|
||||
&:disabled,
|
||||
&[disabled] {
|
||||
border: 1px solid ${(props) => props.theme.colors.background?.inverse};
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:disabled:hover,
|
||||
&[disabled]:hover {
|
||||
background-color: ${(props) => props.theme.colors.background?.inverse};
|
||||
color: ${(props) => props.theme.colors.text?.inverse};
|
||||
border: 1px solid ${(props) => props.theme.colors.background?.inverse};
|
||||
}
|
||||
}
|
||||
`;
|
||||
74
chain138-snap/packages/site/src/hooks/MetamaskContext.tsx
Normal file
74
chain138-snap/packages/site/src/hooks/MetamaskContext.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import type { MetaMaskInpageProvider } from '@metamask/providers';
|
||||
import type { ReactNode } from 'react';
|
||||
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;
|
||||
};
|
||||
|
||||
export const MetaMaskContext = createContext<MetaMaskContextType>({
|
||||
provider: null,
|
||||
installedSnap: null,
|
||||
error: null,
|
||||
setInstalledSnap: () => {
|
||||
/* no-op */
|
||||
},
|
||||
setError: () => {
|
||||
/* no-op */
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* MetaMask context provider to handle MetaMask and snap status.
|
||||
*
|
||||
* @param props - React Props.
|
||||
* @param props.children - React component to be wrapped by the Provider.
|
||||
* @returns JSX.
|
||||
*/
|
||||
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);
|
||||
|
||||
useEffect(() => {
|
||||
getSnapsProvider().then(setProvider).catch(console.error);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
const timeout = setTimeout(() => {
|
||||
setError(null);
|
||||
}, 10000);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<MetaMaskContext.Provider
|
||||
value={{ provider, error, setError, installedSnap, setInstalledSnap }}
|
||||
>
|
||||
{children}
|
||||
</MetaMaskContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility hook to consume the MetaMask context.
|
||||
*
|
||||
* @returns The MetaMask context.
|
||||
*/
|
||||
export function useMetaMaskContext() {
|
||||
return useContext(MetaMaskContext);
|
||||
}
|
||||
5
chain138-snap/packages/site/src/hooks/index.ts
Normal file
5
chain138-snap/packages/site/src/hooks/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './MetamaskContext';
|
||||
export * from './useInvokeSnap';
|
||||
export * from './useMetaMask';
|
||||
export * from './useRequest';
|
||||
export * from './useRequestSnap';
|
||||
37
chain138-snap/packages/site/src/hooks/useInvokeSnap.ts
Normal file
37
chain138-snap/packages/site/src/hooks/useInvokeSnap.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { useRequest } from './useRequest';
|
||||
import { defaultSnapOrigin } from '../config';
|
||||
|
||||
export type InvokeSnapParams = {
|
||||
method: string;
|
||||
params?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility hook to wrap the `wallet_invokeSnap` method.
|
||||
*
|
||||
* @param snapId - The Snap ID to invoke. Defaults to the snap ID specified in the
|
||||
* config.
|
||||
* @returns The invokeSnap wrapper method.
|
||||
*/
|
||||
export const useInvokeSnap = (snapId = defaultSnapOrigin) => {
|
||||
const request = useRequest();
|
||||
|
||||
/**
|
||||
* Invoke the requested Snap method.
|
||||
*
|
||||
* @param params - The invoke params.
|
||||
* @param params.method - The method name.
|
||||
* @param params.params - The method params.
|
||||
* @returns The Snap response.
|
||||
*/
|
||||
const invokeSnap = async ({ method, params }: InvokeSnapParams) =>
|
||||
request({
|
||||
method: 'wallet_invokeSnap',
|
||||
params: {
|
||||
snapId,
|
||||
request: params ? { method, params } : { method },
|
||||
},
|
||||
});
|
||||
|
||||
return invokeSnap;
|
||||
};
|
||||
57
chain138-snap/packages/site/src/hooks/useMetaMask.ts
Normal file
57
chain138-snap/packages/site/src/hooks/useMetaMask.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useMetaMaskContext } from './MetamaskContext';
|
||||
import { useRequest } from './useRequest';
|
||||
import { defaultSnapOrigin } from '../config';
|
||||
import type { GetSnapsResponse } from '../types';
|
||||
|
||||
/**
|
||||
* A hook to retrieve useful data from MetaMask.
|
||||
*
|
||||
* @returns The information.
|
||||
*/
|
||||
export const useMetaMask = () => {
|
||||
const { provider, setInstalledSnap, installedSnap } = useMetaMaskContext();
|
||||
const request = useRequest();
|
||||
|
||||
const [isFlask, setIsFlask] = useState(false);
|
||||
|
||||
const snapsDetected = provider !== null;
|
||||
|
||||
/**
|
||||
* Detect if the version of MetaMask is Flask.
|
||||
*/
|
||||
const detectFlask = async () => {
|
||||
const clientVersion = await request({
|
||||
method: 'web3_clientVersion',
|
||||
});
|
||||
|
||||
const isFlaskDetected = (clientVersion as string[])?.includes('flask');
|
||||
|
||||
setIsFlask(isFlaskDetected);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the Snap informations from MetaMask.
|
||||
*/
|
||||
const getSnap = async () => {
|
||||
const snaps = (await request({
|
||||
method: 'wallet_getSnaps',
|
||||
})) as GetSnapsResponse;
|
||||
|
||||
setInstalledSnap(snaps[defaultSnapOrigin] ?? null);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const detect = async () => {
|
||||
if (provider) {
|
||||
await detectFlask();
|
||||
await getSnap();
|
||||
}
|
||||
};
|
||||
|
||||
detect().catch(console.error);
|
||||
}, [provider]);
|
||||
|
||||
return { isFlask, snapsDetected, installedSnap, getSnap };
|
||||
};
|
||||
40
chain138-snap/packages/site/src/hooks/useRequest.ts
Normal file
40
chain138-snap/packages/site/src/hooks/useRequest.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { RequestArguments } from '@metamask/providers';
|
||||
|
||||
import { useMetaMaskContext } from './MetamaskContext';
|
||||
|
||||
export type Request = (params: RequestArguments) => Promise<unknown | null>;
|
||||
|
||||
/**
|
||||
* Utility hook to consume the provider `request` method with the available provider.
|
||||
*
|
||||
* @returns The `request` function.
|
||||
*/
|
||||
export const useRequest = () => {
|
||||
const { provider, setError } = useMetaMaskContext();
|
||||
|
||||
/**
|
||||
* `provider.request` wrapper.
|
||||
*
|
||||
* @param params - The request params.
|
||||
* @param params.method - The method to call.
|
||||
* @param params.params - The method params.
|
||||
* @returns The result of the request.
|
||||
*/
|
||||
const request: Request = async ({ method, params }) => {
|
||||
try {
|
||||
const data =
|
||||
(await provider?.request({
|
||||
method,
|
||||
params,
|
||||
} as RequestArguments)) ?? null;
|
||||
|
||||
return data;
|
||||
} catch (requestError: any) {
|
||||
setError(requestError);
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return request;
|
||||
};
|
||||
37
chain138-snap/packages/site/src/hooks/useRequestSnap.ts
Normal file
37
chain138-snap/packages/site/src/hooks/useRequestSnap.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { useMetaMaskContext } from './MetamaskContext';
|
||||
import { useRequest } from './useRequest';
|
||||
import { defaultSnapOrigin } from '../config';
|
||||
import type { Snap } from '../types';
|
||||
|
||||
/**
|
||||
* Utility hook to wrap the `wallet_requestSnaps` method.
|
||||
*
|
||||
* @param snapId - The requested Snap ID. Defaults to the snap ID specified in the
|
||||
* config.
|
||||
* @param version - The requested version.
|
||||
* @returns The `wallet_requestSnaps` wrapper.
|
||||
*/
|
||||
export const useRequestSnap = (
|
||||
snapId = defaultSnapOrigin,
|
||||
version?: string,
|
||||
) => {
|
||||
const request = useRequest();
|
||||
const { setInstalledSnap } = useMetaMaskContext();
|
||||
|
||||
/**
|
||||
* Request the Snap.
|
||||
*/
|
||||
const requestSnap = async () => {
|
||||
const snaps = (await request({
|
||||
method: 'wallet_requestSnaps',
|
||||
params: {
|
||||
[snapId]: version ? { version } : {},
|
||||
},
|
||||
})) as Record<string, Snap>;
|
||||
|
||||
// Updates the `installedSnap` context variable since we just installed the Snap.
|
||||
setInstalledSnap(snaps?.[snapId] ?? null);
|
||||
};
|
||||
|
||||
return requestSnap;
|
||||
};
|
||||
496
chain138-snap/packages/site/src/pages/index.tsx
Normal file
496
chain138-snap/packages/site/src/pages/index.tsx
Normal file
@@ -0,0 +1,496 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
ConnectButton,
|
||||
InstallFlaskButton,
|
||||
ReconnectButton,
|
||||
SendHelloButton,
|
||||
Card,
|
||||
} from '../components';
|
||||
import { defaultSnapOrigin, getSnapApiBaseUrl } from '../config';
|
||||
import {
|
||||
useMetaMask,
|
||||
useInvokeSnap,
|
||||
useMetaMaskContext,
|
||||
useRequestSnap,
|
||||
} from '../hooks';
|
||||
import { isLocalSnap, shouldDisplayReconnectButton } from '../utils';
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
margin-top: 7.6rem;
|
||||
margin-bottom: 7.6rem;
|
||||
${({ theme }) => theme.mediaQueries.small} {
|
||||
padding-left: 2.4rem;
|
||||
padding-right: 2.4rem;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
width: auto;
|
||||
}
|
||||
`;
|
||||
|
||||
const Heading = styled.h1`
|
||||
margin-top: 0;
|
||||
margin-bottom: 2.4rem;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const Span = styled.span`
|
||||
color: ${(props) => props.theme.colors.primary?.default};
|
||||
`;
|
||||
|
||||
const Subtitle = styled.p`
|
||||
font-size: ${({ theme }) => theme.fontSizes.large};
|
||||
font-weight: 500;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
${({ theme }) => theme.mediaQueries.small} {
|
||||
font-size: ${({ theme }) => theme.fontSizes.text};
|
||||
}
|
||||
`;
|
||||
|
||||
const CardContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
max-width: 64.8rem;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin-top: 1.5rem;
|
||||
`;
|
||||
|
||||
const Notice = styled.div`
|
||||
background-color: ${({ theme }) => theme.colors.background?.alternative};
|
||||
border: 1px solid ${({ theme }) => theme.colors.border?.default};
|
||||
color: ${({ theme }) => theme.colors.text?.alternative};
|
||||
border-radius: ${({ theme }) => theme.radii.default};
|
||||
padding: 2.4rem;
|
||||
margin-top: 2.4rem;
|
||||
max-width: 60rem;
|
||||
width: 100%;
|
||||
|
||||
& > * {
|
||||
margin: 0;
|
||||
}
|
||||
${({ theme }) => theme.mediaQueries.small} {
|
||||
margin-top: 1.2rem;
|
||||
padding: 1.6rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const ErrorMessage = styled.div`
|
||||
background-color: ${({ theme }) => theme.colors.error?.muted};
|
||||
border: 1px solid ${({ theme }) => theme.colors.error?.default};
|
||||
color: ${({ theme }) => theme.colors.error?.alternative};
|
||||
border-radius: ${({ theme }) => theme.radii.default};
|
||||
padding: 2.4rem;
|
||||
margin-bottom: 2.4rem;
|
||||
margin-top: 2.4rem;
|
||||
max-width: 60rem;
|
||||
width: 100%;
|
||||
${({ theme }) => theme.mediaQueries.small} {
|
||||
padding: 1.6rem;
|
||||
margin-bottom: 1.2rem;
|
||||
margin-top: 1.2rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
const MarketSummaryList = styled.div`
|
||||
font-size: ${({ theme }) => theme.fontSizes.small};
|
||||
max-height: 12rem;
|
||||
overflow-y: auto;
|
||||
margin-top: 0.8rem;
|
||||
`;
|
||||
|
||||
const Index = () => {
|
||||
const { error } = useMetaMaskContext();
|
||||
const { isFlask, snapsDetected, installedSnap } = useMetaMask();
|
||||
const requestSnap = useRequestSnap();
|
||||
const invokeSnap = useInvokeSnap();
|
||||
const apiBaseUrl = getSnapApiBaseUrl();
|
||||
const [marketSummary, setMarketSummary] = useState<{
|
||||
tokens: Array<{
|
||||
symbol?: string;
|
||||
name?: string;
|
||||
address?: string;
|
||||
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('');
|
||||
const [swapTokenOut, setSwapTokenOut] = useState('');
|
||||
const [swapAmountIn, setSwapAmountIn] = useState('');
|
||||
|
||||
const isMetaMaskReady = isLocalSnap(defaultSnapOrigin)
|
||||
? isFlask
|
||||
: snapsDetected;
|
||||
|
||||
const snapParams = apiBaseUrl ? { apiBaseUrl } : undefined;
|
||||
|
||||
const handleSendHelloClick = async () => {
|
||||
await invokeSnap(
|
||||
snapParams ? { method: 'hello', params: snapParams } : { method: 'hello' },
|
||||
);
|
||||
};
|
||||
|
||||
const handleShowMarketData = async () => {
|
||||
if (!apiBaseUrl) {
|
||||
return;
|
||||
}
|
||||
await invokeSnap({
|
||||
method: 'show_market_data',
|
||||
params: { apiBaseUrl },
|
||||
});
|
||||
};
|
||||
|
||||
const handleGetMarketSummary = async () => {
|
||||
if (!apiBaseUrl) {
|
||||
setMarketSummary({ tokens: [], error: 'Set GATSBY_SNAP_API_BASE_URL' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = (await invokeSnap({
|
||||
method: 'get_market_summary',
|
||||
params: { apiBaseUrl },
|
||||
})) as {
|
||||
tokens?: Array<{
|
||||
symbol?: string;
|
||||
name?: string;
|
||||
address?: string;
|
||||
market?: { priceUsd?: number; volume24h?: number };
|
||||
}>;
|
||||
error?: string;
|
||||
};
|
||||
if (result?.error) {
|
||||
setMarketSummary({ tokens: [], error: result.error });
|
||||
} else {
|
||||
setMarketSummary({ tokens: result?.tokens ?? [] });
|
||||
}
|
||||
} catch (e) {
|
||||
setMarketSummary({
|
||||
tokens: [],
|
||||
error: e instanceof Error ? e.message : 'Failed to fetch',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleShowBridgeRoutes = async () => {
|
||||
if (!apiBaseUrl) {
|
||||
return;
|
||||
}
|
||||
await invokeSnap({
|
||||
method: 'show_bridge_routes',
|
||||
params: { apiBaseUrl },
|
||||
});
|
||||
};
|
||||
|
||||
const handleGetSwapQuote = async () => {
|
||||
if (!apiBaseUrl) {
|
||||
setSwapQuote({ error: 'Set GATSBY_SNAP_API_BASE_URL' });
|
||||
return;
|
||||
}
|
||||
if (!swapTokenIn || !swapTokenOut || !swapAmountIn) {
|
||||
setSwapQuote({ error: 'Enter tokenIn, tokenOut, and amountIn' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = (await invokeSnap({
|
||||
method: 'get_swap_quote',
|
||||
params: {
|
||||
apiBaseUrl,
|
||||
chainId: 138,
|
||||
tokenIn: swapTokenIn.trim(),
|
||||
tokenOut: swapTokenOut.trim(),
|
||||
amountIn: swapAmountIn.trim(),
|
||||
},
|
||||
})) as {
|
||||
amountOut?: string;
|
||||
error?: string;
|
||||
poolAddress?: string | null;
|
||||
};
|
||||
setSwapQuote({
|
||||
amountOut: result?.amountOut,
|
||||
error: result?.error,
|
||||
poolAddress: result?.poolAddress,
|
||||
});
|
||||
} catch (e) {
|
||||
setSwapQuote({
|
||||
error: e instanceof Error ? e.message : 'Failed to fetch quote',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleShowSwapQuote = async () => {
|
||||
if (!apiBaseUrl || !swapTokenIn || !swapTokenOut || !swapAmountIn) {
|
||||
return;
|
||||
}
|
||||
await invokeSnap({
|
||||
method: 'show_swap_quote',
|
||||
params: {
|
||||
apiBaseUrl,
|
||||
chainId: 138,
|
||||
tokenIn: swapTokenIn.trim(),
|
||||
tokenOut: swapTokenOut.trim(),
|
||||
amountIn: swapAmountIn.trim(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Heading>
|
||||
Welcome to <Span>template-snap</Span>
|
||||
</Heading>
|
||||
<Subtitle>
|
||||
Get started by editing <code>src/index.tsx</code>
|
||||
</Subtitle>
|
||||
<Notice>
|
||||
<p>
|
||||
<strong>How to use:</strong> Install MetaMask (or MetaMask Flask for
|
||||
Snaps) → Connect your wallet → Install the Snap → Use the Market,
|
||||
Bridge, and Swap cards below (set GATSBY_SNAP_API_BASE_URL for
|
||||
production API).
|
||||
</p>
|
||||
</Notice>
|
||||
<CardContainer>
|
||||
{error && (
|
||||
<ErrorMessage>
|
||||
<b>An error happened:</b> {error.message}
|
||||
</ErrorMessage>
|
||||
)}
|
||||
{!isMetaMaskReady && (
|
||||
<Card
|
||||
content={{
|
||||
title: 'Install',
|
||||
description:
|
||||
'Snaps is pre-release software only available in MetaMask Flask, a canary distribution for developers with access to upcoming features.',
|
||||
button: <InstallFlaskButton />,
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
{!installedSnap && (
|
||||
<Card
|
||||
content={{
|
||||
title: 'Connect',
|
||||
description:
|
||||
'Get started by connecting to and installing the example snap.',
|
||||
button: (
|
||||
<ConnectButton
|
||||
onClick={requestSnap}
|
||||
disabled={!isMetaMaskReady}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
disabled={!isMetaMaskReady}
|
||||
/>
|
||||
)}
|
||||
{shouldDisplayReconnectButton(installedSnap) && (
|
||||
<Card
|
||||
content={{
|
||||
title: 'Reconnect',
|
||||
description:
|
||||
'While connected to a local running snap this button will always be displayed in order to update the snap if a change is made.',
|
||||
button: (
|
||||
<ReconnectButton
|
||||
onClick={requestSnap}
|
||||
disabled={!installedSnap}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
disabled={!installedSnap}
|
||||
/>
|
||||
)}
|
||||
<Card
|
||||
content={{
|
||||
title: 'Send Hello message',
|
||||
description:
|
||||
'Display a custom message within a confirmation screen in MetaMask.',
|
||||
button: (
|
||||
<SendHelloButton
|
||||
onClick={handleSendHelloClick}
|
||||
disabled={!installedSnap}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
disabled={!installedSnap}
|
||||
fullWidth={
|
||||
isMetaMaskReady &&
|
||||
Boolean(installedSnap) &&
|
||||
!shouldDisplayReconnectButton(installedSnap)
|
||||
}
|
||||
/>
|
||||
<Card
|
||||
content={{
|
||||
title: 'Market data',
|
||||
description: apiBaseUrl
|
||||
? 'Show token prices in a Snap dialog (from token-aggregation API).'
|
||||
: 'Set GATSBY_SNAP_API_BASE_URL to show market data.',
|
||||
button: (
|
||||
<Button
|
||||
onClick={handleShowMarketData}
|
||||
disabled={!installedSnap || !apiBaseUrl}
|
||||
>
|
||||
Show market data
|
||||
</Button>
|
||||
),
|
||||
}}
|
||||
disabled={!installedSnap}
|
||||
/>
|
||||
<Card
|
||||
content={{
|
||||
title: 'Market summary',
|
||||
description: apiBaseUrl
|
||||
? 'Fetch token list with prices and display below.'
|
||||
: 'Set GATSBY_SNAP_API_BASE_URL to fetch market summary.',
|
||||
button: (
|
||||
<Button
|
||||
onClick={handleGetMarketSummary}
|
||||
disabled={!installedSnap || !apiBaseUrl}
|
||||
>
|
||||
Fetch market summary
|
||||
</Button>
|
||||
),
|
||||
}}
|
||||
disabled={!installedSnap}
|
||||
/>
|
||||
<Card
|
||||
content={{
|
||||
title: 'Bridge',
|
||||
description: apiBaseUrl
|
||||
? 'Show CCIP bridge routes (WETH9 / WETH10) in a Snap dialog. Use explorer for executing transfers.'
|
||||
: 'Set GATSBY_SNAP_API_BASE_URL to show bridge routes.',
|
||||
button: (
|
||||
<Button
|
||||
onClick={handleShowBridgeRoutes}
|
||||
disabled={!installedSnap || !apiBaseUrl}
|
||||
>
|
||||
Show bridge routes
|
||||
</Button>
|
||||
),
|
||||
}}
|
||||
disabled={!installedSnap}
|
||||
/>
|
||||
<Card
|
||||
fullWidth
|
||||
content={{
|
||||
title: 'Swap quote',
|
||||
description: apiBaseUrl ? (
|
||||
<div>
|
||||
<div style={{ marginBottom: '0.5rem' }}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Token In (address)"
|
||||
value={swapTokenIn}
|
||||
onChange={(e) => setSwapTokenIn(e.target.value)}
|
||||
style={{ width: '100%', marginBottom: '0.25rem' }}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Token Out (address)"
|
||||
value={swapTokenOut}
|
||||
onChange={(e) => setSwapTokenOut(e.target.value)}
|
||||
style={{ width: '100%', marginBottom: '0.25rem' }}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Amount In (raw)"
|
||||
value={swapAmountIn}
|
||||
onChange={(e) => setSwapAmountIn(e.target.value)}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
|
||||
<Button
|
||||
onClick={handleGetSwapQuote}
|
||||
disabled={!installedSnap}
|
||||
>
|
||||
Get quote
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleShowSwapQuote}
|
||||
disabled={
|
||||
!installedSnap ||
|
||||
!swapTokenIn ||
|
||||
!swapTokenOut ||
|
||||
!swapAmountIn
|
||||
}
|
||||
>
|
||||
Show quote in Snap
|
||||
</Button>
|
||||
</div>
|
||||
{swapQuote && (
|
||||
<div style={{ marginTop: '0.8rem', fontSize: '0.9rem' }}>
|
||||
{swapQuote.error ? (
|
||||
<span style={{ color: 'var(--color-error-default)' }}>
|
||||
{swapQuote.error}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
Amount Out: {swapQuote.amountOut ?? '—'}
|
||||
{swapQuote.poolAddress && (
|
||||
<> (pool: {swapQuote.poolAddress.slice(0, 10)}...)</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
'Set GATSBY_SNAP_API_BASE_URL and enter token addresses + amount (raw).'
|
||||
),
|
||||
button: null,
|
||||
}}
|
||||
disabled={!installedSnap}
|
||||
/>
|
||||
{marketSummary && (
|
||||
<Card
|
||||
fullWidth
|
||||
content={{
|
||||
title: 'Market summary result',
|
||||
description: marketSummary.error ? (
|
||||
<span style={{ color: 'var(--color-error-default)' }}>
|
||||
{marketSummary.error}
|
||||
</span>
|
||||
) : (
|
||||
<MarketSummaryList>
|
||||
{marketSummary.tokens.length === 0
|
||||
? 'No tokens.'
|
||||
: marketSummary.tokens.map((t, i) => (
|
||||
<div key={i}>
|
||||
{t.symbol ?? t.name ?? 'Token'}
|
||||
{t.market?.priceUsd != null &&
|
||||
` — $${Number(t.market.priceUsd).toFixed(4)}`}
|
||||
</div>
|
||||
))}
|
||||
</MarketSummaryList>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<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.
|
||||
</p>
|
||||
</Notice>
|
||||
</CardContainer>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
||||
25
chain138-snap/packages/site/src/types/custom.d.ts
vendored
Normal file
25
chain138-snap/packages/site/src/types/custom.d.ts
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
import type {
|
||||
EIP6963AnnounceProviderEvent,
|
||||
EIP6963RequestProviderEvent,
|
||||
MetaMaskInpageProvider,
|
||||
} from '@metamask/providers';
|
||||
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
1
chain138-snap/packages/site/src/types/index.ts
Normal file
1
chain138-snap/packages/site/src/types/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { type GetSnapsResponse, type Snap } from './snap';
|
||||
8
chain138-snap/packages/site/src/types/snap.ts
Normal file
8
chain138-snap/packages/site/src/types/snap.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export type GetSnapsResponse = Record<string, Snap>;
|
||||
|
||||
export type Snap = {
|
||||
permissionName: string;
|
||||
id: string;
|
||||
version: string;
|
||||
initialPermissions: Record<string, unknown>;
|
||||
};
|
||||
19
chain138-snap/packages/site/src/types/styled.d.ts
vendored
Normal file
19
chain138-snap/packages/site/src/types/styled.d.ts
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
/* eslint-disable import-x/no-unassigned-import */
|
||||
|
||||
import 'styled-components';
|
||||
|
||||
/**
|
||||
* styled-component default theme extension
|
||||
*/
|
||||
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>>;
|
||||
}
|
||||
}
|
||||
7
chain138-snap/packages/site/src/types/svg.d.ts
vendored
Normal file
7
chain138-snap/packages/site/src/types/svg.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
/* eslint-disable import-x/unambiguous */
|
||||
|
||||
declare module '*.svg' {
|
||||
import type { FunctionComponent, SVGProps } from 'react';
|
||||
|
||||
export const ReactComponent: FunctionComponent<SVGProps<SVGSVGElement>>;
|
||||
}
|
||||
5
chain138-snap/packages/site/src/utils/button.ts
Normal file
5
chain138-snap/packages/site/src/utils/button.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { isLocalSnap } from './snap';
|
||||
import type { Snap } from '../types';
|
||||
|
||||
export const shouldDisplayReconnectButton = (installedSnap: Snap | null) =>
|
||||
installedSnap && isLocalSnap(installedSnap?.id);
|
||||
5
chain138-snap/packages/site/src/utils/index.ts
Normal file
5
chain138-snap/packages/site/src/utils/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './metamask';
|
||||
export * from './snap';
|
||||
export * from './theme';
|
||||
export * from './localStorage';
|
||||
export * from './button';
|
||||
33
chain138-snap/packages/site/src/utils/localStorage.ts
Normal file
33
chain138-snap/packages/site/src/utils/localStorage.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Get a local storage key.
|
||||
*
|
||||
* @param key - The local storage key to access.
|
||||
* @returns The value stored at the key provided if the key exists.
|
||||
*/
|
||||
export const getLocalStorage = (key: string) => {
|
||||
const { localStorage: ls } = window;
|
||||
|
||||
if (ls !== null) {
|
||||
const data = ls.getItem(key);
|
||||
return data;
|
||||
}
|
||||
|
||||
throw new Error('Local storage is not available.');
|
||||
};
|
||||
|
||||
/**
|
||||
* Set a value to local storage at a certain key.
|
||||
*
|
||||
* @param key - The local storage key to set.
|
||||
* @param value - The value to set.
|
||||
*/
|
||||
export const setLocalStorage = (key: string, value: string) => {
|
||||
const { localStorage: ls } = window;
|
||||
|
||||
if (ls !== null) {
|
||||
ls.setItem(key, value);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error('Local storage is not available.');
|
||||
};
|
||||
120
chain138-snap/packages/site/src/utils/metamask.ts
Normal file
120
chain138-snap/packages/site/src/utils/metamask.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import type {
|
||||
EIP6963AnnounceProviderEvent,
|
||||
MetaMaskInpageProvider,
|
||||
} from '@metamask/providers';
|
||||
|
||||
/**
|
||||
* Check if the current provider supports snaps by calling `wallet_getSnaps`.
|
||||
*
|
||||
* @param provider - The provider to use to check for snaps support. Defaults to
|
||||
* `window.ethereum`.
|
||||
* @returns True if the provider supports snaps, false otherwise.
|
||||
*/
|
||||
export async function hasSnapsSupport(
|
||||
provider: MetaMaskInpageProvider = window.ethereum,
|
||||
) {
|
||||
try {
|
||||
await provider.request({
|
||||
method: 'wallet_getSnaps',
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a MetaMask provider using EIP6963. This will return the first provider
|
||||
* reporting as MetaMask. If no provider is found after 500ms, this will
|
||||
* return null instead.
|
||||
*
|
||||
* @returns A MetaMask provider if found, otherwise null.
|
||||
*/
|
||||
export async function getMetaMaskEIP6963Provider() {
|
||||
return new Promise<MetaMaskInpageProvider | null>((resolve) => {
|
||||
// Timeout looking for providers after 500ms
|
||||
const timeout = setTimeout(() => {
|
||||
resolveWithCleanup(null);
|
||||
}, 500);
|
||||
|
||||
/**
|
||||
* Resolve the promise with a MetaMask provider and clean up.
|
||||
*
|
||||
* @param provider - A MetaMask provider if found, otherwise null.
|
||||
*/
|
||||
function resolveWithCleanup(provider: MetaMaskInpageProvider | null) {
|
||||
window.removeEventListener(
|
||||
'eip6963:announceProvider',
|
||||
onAnnounceProvider,
|
||||
);
|
||||
|
||||
clearTimeout(timeout);
|
||||
resolve(provider);
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener for the EIP6963 announceProvider event.
|
||||
*
|
||||
* Resolves the promise if a MetaMask provider is found.
|
||||
*
|
||||
* @param event - The EIP6963 announceProvider event.
|
||||
* @param event.detail - The details of the EIP6963 announceProvider event.
|
||||
*/
|
||||
function onAnnounceProvider({ detail }: EIP6963AnnounceProviderEvent) {
|
||||
if (!detail) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { info, provider } = detail;
|
||||
|
||||
if (info.rdns.includes('io.metamask')) {
|
||||
resolveWithCleanup(provider);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('eip6963:announceProvider', onAnnounceProvider);
|
||||
|
||||
window.dispatchEvent(new Event('eip6963:requestProvider'));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a provider that supports snaps. This will loop through all the detected
|
||||
* providers and return the first one that supports snaps.
|
||||
*
|
||||
* @returns The provider, or `null` if no provider supports snaps.
|
||||
*/
|
||||
export async function getSnapsProvider() {
|
||||
if (typeof window === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (await hasSnapsSupport()) {
|
||||
return window.ethereum;
|
||||
}
|
||||
|
||||
if (window.ethereum?.detected) {
|
||||
for (const provider of window.ethereum.detected) {
|
||||
if (await hasSnapsSupport(provider)) {
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (window.ethereum?.providers) {
|
||||
for (const provider of window.ethereum.providers) {
|
||||
if (await hasSnapsSupport(provider)) {
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const eip6963Provider = await getMetaMaskEIP6963Provider();
|
||||
|
||||
if (eip6963Provider && (await hasSnapsSupport(eip6963Provider))) {
|
||||
return eip6963Provider;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
7
chain138-snap/packages/site/src/utils/snap.ts
Normal file
7
chain138-snap/packages/site/src/utils/snap.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Check if a snap ID is a local snap ID.
|
||||
*
|
||||
* @param snapId - The snap ID.
|
||||
* @returns True if it's a local Snap, or false otherwise.
|
||||
*/
|
||||
export const isLocalSnap = (snapId: string) => snapId.startsWith('local:');
|
||||
27
chain138-snap/packages/site/src/utils/theme.ts
Normal file
27
chain138-snap/packages/site/src/utils/theme.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { getLocalStorage, setLocalStorage } from './localStorage';
|
||||
|
||||
/**
|
||||
* Get the user's preferred theme in local storage.
|
||||
* Will default to the browser's preferred theme if there is no value in local storage.
|
||||
*
|
||||
* @returns True if the theme is "dark" otherwise, false.
|
||||
*/
|
||||
export const getThemePreference = () => {
|
||||
if (typeof window === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const darkModeSystem = window?.matchMedia(
|
||||
'(prefers-color-scheme: dark)',
|
||||
).matches;
|
||||
|
||||
const localStoragePreference = getLocalStorage('theme');
|
||||
const systemPreference = darkModeSystem ? 'dark' : 'light';
|
||||
const preference = localStoragePreference ?? systemPreference;
|
||||
|
||||
if (!localStoragePreference) {
|
||||
setLocalStorage('theme', systemPreference);
|
||||
}
|
||||
|
||||
return preference === 'dark';
|
||||
};
|
||||
3
chain138-snap/packages/site/static/robots.txt
Normal file
3
chain138-snap/packages/site/static/robots.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
9
chain138-snap/packages/site/tsconfig.json
Normal file
9
chain138-snap/packages/site/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"resolveJsonModule": true,
|
||||
"jsx": "preserve"
|
||||
},
|
||||
"include": ["src", "gatsby-browser.tsx", "gatsby-config.ts", "gatsby-ssr.tsx"]
|
||||
}
|
||||
12
chain138-snap/packages/snap/README.md
Normal file
12
chain138-snap/packages/snap/README.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# TypeScript Example Snap
|
||||
|
||||
This snap demonstrates how to develop a snap with TypeScript. It is a simple
|
||||
snap that displays a confirmation dialog when the `hello` JSON-RPC method is
|
||||
called.
|
||||
|
||||
## Testing
|
||||
|
||||
The snap comes with some basic tests, to demonstrate how to write tests for
|
||||
snaps. To test the snap, run **pnpm run test** (or **yarn test**) from the repo root, or `pnpm run test` / `yarn test` in this directory. This will use
|
||||
[`@metamask/snaps-jest`](https://github.com/MetaMask/snaps/tree/main/packages/snaps-jest)
|
||||
to run the tests in `src/index.test.ts`.
|
||||
10
chain138-snap/packages/snap/images/icon.svg
Normal file
10
chain138-snap/packages/snap/images/icon.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#6366f1"/>
|
||||
<stop offset="100%" style="stop-color:#4f46e5"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="64" height="64" rx="12" fill="url(#bg)"/>
|
||||
<text x="32" y="42" font-family="system-ui, sans-serif" font-size="28" font-weight="700" fill="white" text-anchor="middle">138</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 498 B |
6
chain138-snap/packages/snap/jest.config.js
Normal file
6
chain138-snap/packages/snap/jest.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
preset: '@metamask/snaps-jest',
|
||||
transform: {
|
||||
'^.+\\.(t|j)sx?$': 'ts-jest',
|
||||
},
|
||||
};
|
||||
53
chain138-snap/packages/snap/package.json
Normal file
53
chain138-snap/packages/snap/package.json
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"name": "snap",
|
||||
"version": "0.1.0",
|
||||
"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/MetaMask/template-snap-monorepo.git"
|
||||
},
|
||||
"license": "(MIT-0 OR Apache-2.0)",
|
||||
"main": "./dist/bundle.js",
|
||||
"files": [
|
||||
"dist/",
|
||||
"images/",
|
||||
"snap.manifest.json"
|
||||
],
|
||||
"scripts": {
|
||||
"allow-scripts": "pnpm --filter root run allow-scripts",
|
||||
"build": "mm-snap build",
|
||||
"build:clean": "pnpm run clean && pnpm run build",
|
||||
"clean": "rimraf dist",
|
||||
"lint": "yarn lint:eslint && yarn lint:misc --check",
|
||||
"lint:eslint": "eslint . --cache --ext js,ts",
|
||||
"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",
|
||||
"prepublishOnly": "mm-snap manifest",
|
||||
"serve": "mm-snap serve",
|
||||
"start": "mm-snap watch",
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@metamask/snaps-sdk": "10.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "^29.5.0",
|
||||
"@metamask/providers": "^22.1.0",
|
||||
"@metamask/snaps-cli": "^8.3.0",
|
||||
"@metamask/snaps-jest": "^9.8.0",
|
||||
"@types/react": "18.2.4",
|
||||
"@types/react-dom": "18.2.4",
|
||||
"eslint": "^9.39.2",
|
||||
"jest": "^29.5.0",
|
||||
"rimraf": "^5.0.5",
|
||||
"ts-jest": "^29.1.0",
|
||||
"typescript": "~5.9.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.6.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
}
|
||||
}
|
||||
14
chain138-snap/packages/snap/snap.config.ts
Normal file
14
chain138-snap/packages/snap/snap.config.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { SnapConfig } from '@metamask/snaps-cli';
|
||||
import { resolve } from 'path';
|
||||
|
||||
const config: SnapConfig = {
|
||||
input: resolve(__dirname, 'src/index.tsx'),
|
||||
server: {
|
||||
port: 8080,
|
||||
},
|
||||
polyfills: {
|
||||
buffer: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
30
chain138-snap/packages/snap/snap.manifest.json
Normal file
30
chain138-snap/packages/snap/snap.manifest.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"version": "0.1.0",
|
||||
"description": "Chain 138 (DeFi Oracle Meta Mainnet) and ALL Mainnet: networks, token list, market data, swap quotes, and CCIP bridge routes for MetaMask.",
|
||||
"proposedName": "Chain 138",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/MetaMask/template-snap-monorepo.git"
|
||||
},
|
||||
"source": {
|
||||
"shasum": "gLlFZx1q+vaYM4tYKOg0hHlIDB9p6GdoZ4jw3ZU7jYg=",
|
||||
"location": {
|
||||
"npm": {
|
||||
"filePath": "dist/bundle.js",
|
||||
"iconPath": "images/icon.svg",
|
||||
"packageName": "snap",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
}
|
||||
}
|
||||
},
|
||||
"initialPermissions": {
|
||||
"snap_dialog": {},
|
||||
"endowment:rpc": {
|
||||
"dapps": true,
|
||||
"snaps": false
|
||||
},
|
||||
"endowment:network-access": {}
|
||||
},
|
||||
"platformVersion": "10.3.0",
|
||||
"manifestVersion": "0.1"
|
||||
}
|
||||
56
chain138-snap/packages/snap/src/index.test.tsx
Normal file
56
chain138-snap/packages/snap/src/index.test.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { expect } from '@jest/globals';
|
||||
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;
|
||||
};
|
||||
|
||||
describe('onRpcRequest', () => {
|
||||
describe('hello', () => {
|
||||
it('shows a confirmation dialog', async () => {
|
||||
const { request } = await installSnap();
|
||||
|
||||
const origin = 'Jest';
|
||||
const response = request({
|
||||
method: 'hello',
|
||||
origin,
|
||||
});
|
||||
|
||||
const ui = (await response.getInterface()) as SnapConfirmationInterface;
|
||||
expect(ui.type).toBe('confirmation');
|
||||
(expect(ui) as unknown as SnapsJestExpect).toRender(
|
||||
<Box>
|
||||
<Text>
|
||||
Hello, <Bold>{origin}</Bold>!
|
||||
</Text>
|
||||
<Text>Chain 138 (DeFi Oracle Meta Mainnet) Snap.</Text>
|
||||
<Text>
|
||||
Use get_networks or get_chain138_config for wallet_addEthereumChain.
|
||||
</Text>
|
||||
</Box>,
|
||||
);
|
||||
|
||||
await ui.ok();
|
||||
|
||||
(expect(await response) as unknown as SnapsJestExpect).toRespondWith(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('throws an error if the requested method does not exist', async () => {
|
||||
const { request } = await installSnap();
|
||||
|
||||
const response = await request({
|
||||
method: 'foo',
|
||||
});
|
||||
|
||||
(expect(response) as unknown as SnapsJestExpect).toRespondWithError({
|
||||
code: -32603,
|
||||
message: 'Method not found.',
|
||||
stack: expect.any(String),
|
||||
});
|
||||
});
|
||||
});
|
||||
741
chain138-snap/packages/snap/src/index.tsx
Normal file
741
chain138-snap/packages/snap/src/index.tsx
Normal file
@@ -0,0 +1,741 @@
|
||||
import type { OnRpcRequestHandler } from '@metamask/snaps-sdk';
|
||||
import { Box, Text, Bold } from '@metamask/snaps-sdk/jsx';
|
||||
|
||||
/** Token-aggregation or market API base URL; dapp passes apiBaseUrl for dynamic config */
|
||||
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;
|
||||
/** When set, get_token_list / get_token_list_url use this URL instead of apiBaseUrl */
|
||||
tokenListUrl?: string;
|
||||
/** When set, get_bridge_routes / show_bridge_routes fetch this URL instead of apiBaseUrl */
|
||||
bridgeListUrl?: string;
|
||||
chainId?: number;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get API base URL from request params, trimmed of trailing slash.
|
||||
*
|
||||
* @param params - Request params with optional apiBaseUrl.
|
||||
* @returns API base URL string.
|
||||
*/
|
||||
function getApiBase(params: { apiBaseUrl?: string } | undefined): string {
|
||||
return (params?.apiBaseUrl ?? DEFAULT_MARKET_API_BASE).replace(/\/$/u, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch networks from token-aggregation API.
|
||||
*
|
||||
* @param apiBase - API base URL.
|
||||
* @returns Networks response with version and networks array.
|
||||
*/
|
||||
async function fetchNetworks(apiBase: string) {
|
||||
const res = await fetch(`${apiBase}/api/v1/networks`);
|
||||
if (!res.ok) {
|
||||
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 };
|
||||
blockExplorerUrls: string[];
|
||||
iconUrls?: string[];
|
||||
oracles?: { name: string; address: string }[];
|
||||
}[];
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming JSON-RPC requests, sent through `wallet_invokeSnap`.
|
||||
*
|
||||
* @param options - RPC request options.
|
||||
* @param options.origin - Origin of the request.
|
||||
* @param options.request - JSON-RPC request object.
|
||||
* @returns Response for the RPC method.
|
||||
*/
|
||||
export const onRpcRequest: OnRpcRequestHandler = async ({
|
||||
origin,
|
||||
request,
|
||||
}) => {
|
||||
const params = (request.params as SnapRpcParams | undefined) ?? {};
|
||||
const base = getApiBase(params);
|
||||
const networksUrl = params.networksUrl?.trim();
|
||||
const tokenListUrl = params.tokenListUrl?.trim();
|
||||
const bridgeListUrl = params.bridgeListUrl?.trim();
|
||||
|
||||
switch (request.method) {
|
||||
case 'get_networks': {
|
||||
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[] };
|
||||
return { version: data.version, networks: data.networks ?? [] };
|
||||
} catch (error) {
|
||||
return {
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch networks URL',
|
||||
version: undefined,
|
||||
networks: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
if (!base) {
|
||||
return {
|
||||
error:
|
||||
'Pass apiBaseUrl or networksUrl to fetch networks',
|
||||
version: undefined,
|
||||
networks: [],
|
||||
};
|
||||
}
|
||||
try {
|
||||
const data = await fetchNetworks(base);
|
||||
return { version: data.version, networks: data.networks ?? [] };
|
||||
} catch (error) {
|
||||
return {
|
||||
error:
|
||||
error instanceof Error ? error.message : 'Failed to fetch networks',
|
||||
version: undefined,
|
||||
networks: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
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 }[] };
|
||||
return data.networks ?? [];
|
||||
}
|
||||
if (base) {
|
||||
const data = await fetchNetworks(base);
|
||||
return data.networks ?? [];
|
||||
}
|
||||
return null;
|
||||
};
|
||||
try {
|
||||
const networks = await loadNetworks();
|
||||
if (networks === null) {
|
||||
return { error: 'Pass apiBaseUrl or networksUrl to fetch chain config' };
|
||||
}
|
||||
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,
|
||||
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,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
error:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Failed to fetch chain config',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
case 'get_chain138_market_chains': {
|
||||
if (!base) {
|
||||
return {
|
||||
error:
|
||||
'Pass apiBaseUrl (token-aggregation service URL) to fetch market chains',
|
||||
chains: [],
|
||||
};
|
||||
}
|
||||
try {
|
||||
const res = await fetch(`${base}/api/v1/chains`);
|
||||
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 chains',
|
||||
chains: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
case 'get_token_list_url': {
|
||||
if (tokenListUrl) {
|
||||
return {
|
||||
tokenListUrl,
|
||||
description:
|
||||
'Add this URL in MetaMask Settings -> Token list to get dynamic token list for Chain 138 and ALL Mainnet.',
|
||||
};
|
||||
}
|
||||
if (!base) {
|
||||
return {
|
||||
error: 'Pass apiBaseUrl or tokenListUrl to get token list URL',
|
||||
tokenListUrl: undefined,
|
||||
};
|
||||
}
|
||||
return {
|
||||
tokenListUrl: `${base}/api/v1/report/token-list`,
|
||||
description:
|
||||
'Add this URL in MetaMask Settings -> Token list to get dynamic token list for Chain 138 and ALL Mainnet.',
|
||||
};
|
||||
}
|
||||
|
||||
case 'get_token_list': {
|
||||
if (tokenListUrl) {
|
||||
const chainIdParam = params?.chainId as number | undefined;
|
||||
const url = chainIdParam
|
||||
? `${tokenListUrl}${tokenListUrl.includes('?') ? '&' : '?'}chainId=${chainIdParam}`
|
||||
: tokenListUrl;
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
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',
|
||||
tokens: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
if (!base) {
|
||||
return {
|
||||
error: 'Pass apiBaseUrl or tokenListUrl to fetch token list',
|
||||
tokens: [],
|
||||
};
|
||||
}
|
||||
const chainIdParam = params?.chainId as number | undefined;
|
||||
const url = chainIdParam
|
||||
? `${base}/api/v1/report/token-list?chainId=${chainIdParam}`
|
||||
: `${base}/api/v1/report/token-list`;
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
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',
|
||||
tokens: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
case 'get_oracles': {
|
||||
if (!base) {
|
||||
return { error: 'Pass apiBaseUrl to fetch oracles config', chains: [] };
|
||||
}
|
||||
const chainIdParam = params?.chainId as number | undefined;
|
||||
const configUrl = chainIdParam
|
||||
? `${base}/api/v1/config?chainId=${chainIdParam}`
|
||||
: `${base}/api/v1/config`;
|
||||
try {
|
||||
const res = await fetch(configUrl);
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}`);
|
||||
}
|
||||
return await res.json();
|
||||
} catch (error) {
|
||||
return {
|
||||
error:
|
||||
error instanceof Error ? error.message : 'Failed to fetch oracles',
|
||||
chains: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
case 'show_dynamic_info': {
|
||||
const hasAny = base || networksUrl || tokenListUrl;
|
||||
if (!hasAny) {
|
||||
return snap.request({
|
||||
method: 'snap_dialog',
|
||||
params: {
|
||||
type: 'alert',
|
||||
content: (
|
||||
<Box>
|
||||
<Text>
|
||||
Pass apiBaseUrl, networksUrl, or tokenListUrl to see dynamic networks and token list URL.
|
||||
</Text>
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
try {
|
||||
let chainNames = 'N/A';
|
||||
if (networksUrl) {
|
||||
const res = await fetch(networksUrl);
|
||||
if (res.ok) {
|
||||
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';
|
||||
}
|
||||
} else if (base) {
|
||||
const data = await fetchNetworks(base);
|
||||
const networks = data.networks ?? [];
|
||||
chainNames = networks.map((net) => `${net.chainName} (${net.chainIdDecimal})`).join(', ') || 'None';
|
||||
}
|
||||
const displayTokenListUrl = tokenListUrl || (base ? `${base}/api/v1/report/token-list` : '');
|
||||
return snap.request({
|
||||
method: 'snap_dialog',
|
||||
params: {
|
||||
type: 'confirmation',
|
||||
content: (
|
||||
<Box>
|
||||
<Text>
|
||||
<Bold>Dynamic networks</Bold>
|
||||
</Text>
|
||||
<Text>{chainNames}</Text>
|
||||
<Text>
|
||||
<Bold>Token list URL</Bold>
|
||||
</Text>
|
||||
<Text>{displayTokenListUrl || 'N/A'}</Text>
|
||||
<Text>
|
||||
Add the URL in MetaMask Settings - Token list for
|
||||
auto-updating tokens.
|
||||
</Text>
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return snap.request({
|
||||
method: 'snap_dialog',
|
||||
params: {
|
||||
type: 'alert',
|
||||
content: (
|
||||
<Box>
|
||||
<Text>
|
||||
Failed to fetch:{' '}
|
||||
{error instanceof Error ? error.message : 'Unknown error'}
|
||||
</Text>
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
case 'get_market_summary': {
|
||||
if (!base) {
|
||||
return {
|
||||
error:
|
||||
'Pass apiBaseUrl (token-aggregation service URL) to fetch market summary',
|
||||
tokens: [],
|
||||
};
|
||||
}
|
||||
const chainIdParam = (params?.chainId as number | undefined) ?? 138;
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${base}/api/v1/tokens?chainId=${chainIdParam}&limit=50`,
|
||||
);
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}`);
|
||||
}
|
||||
const data = (await res.json()) as {
|
||||
tokens?: Array<{
|
||||
symbol?: string;
|
||||
name?: string;
|
||||
address?: string;
|
||||
market?: { priceUsd?: number; volume24h?: number };
|
||||
}>;
|
||||
};
|
||||
const tokens = (data.tokens ?? []).map((t) => ({
|
||||
symbol: t.symbol,
|
||||
name: t.name,
|
||||
address: t.address,
|
||||
market: t.market
|
||||
? { priceUsd: t.market.priceUsd, volume24h: t.market.volume24h }
|
||||
: undefined,
|
||||
}));
|
||||
return { tokens };
|
||||
} catch (error) {
|
||||
return {
|
||||
error:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Failed to fetch market summary',
|
||||
tokens: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
case 'get_swap_quote': {
|
||||
if (!base) {
|
||||
return {
|
||||
error:
|
||||
'Pass apiBaseUrl (token-aggregation service URL) to fetch swap quote',
|
||||
amountOut: undefined,
|
||||
};
|
||||
}
|
||||
const chainIdParam = (params?.chainId as number | undefined) ?? 138;
|
||||
const tokenIn = params?.tokenIn as string | undefined;
|
||||
const tokenOut = params?.tokenOut as string | undefined;
|
||||
const amountIn = params?.amountIn as string | undefined;
|
||||
if (!tokenIn || !tokenOut || amountIn === undefined) {
|
||||
return {
|
||||
error: 'Missing params: tokenIn, tokenOut, amountIn',
|
||||
amountOut: undefined,
|
||||
};
|
||||
}
|
||||
try {
|
||||
const url = new URL(`${base}/api/v1/quote`);
|
||||
url.searchParams.set('chainId', String(chainIdParam));
|
||||
url.searchParams.set('tokenIn', tokenIn);
|
||||
url.searchParams.set('tokenOut', tokenOut);
|
||||
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) {
|
||||
return {
|
||||
error: data.error ?? `HTTP ${res.status}`,
|
||||
amountOut: undefined,
|
||||
};
|
||||
}
|
||||
return {
|
||||
amountOut: data.amountOut ?? undefined,
|
||||
error: data.error,
|
||||
poolAddress: data.poolAddress,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
error:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Failed to fetch swap quote',
|
||||
amountOut: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
case 'show_swap_quote': {
|
||||
if (!base) {
|
||||
return snap.request({
|
||||
method: 'snap_dialog',
|
||||
params: {
|
||||
type: 'alert',
|
||||
content: (
|
||||
<Box>
|
||||
<Text>
|
||||
Pass apiBaseUrl to see swap quote (tokenIn, tokenOut,
|
||||
amountIn).
|
||||
</Text>
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
const chainIdParam = (params?.chainId as number | undefined) ?? 138;
|
||||
const tokenIn = params?.tokenIn as string | undefined;
|
||||
const tokenOut = params?.tokenOut as string | undefined;
|
||||
const amountIn = params?.amountIn as string | undefined;
|
||||
if (!tokenIn || !tokenOut || amountIn === undefined) {
|
||||
return snap.request({
|
||||
method: 'snap_dialog',
|
||||
params: {
|
||||
type: 'alert',
|
||||
content: (
|
||||
<Box>
|
||||
<Text>
|
||||
Missing params: tokenIn, tokenOut, amountIn. Pass them to
|
||||
show_swap_quote.
|
||||
</Text>
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
try {
|
||||
const url = new URL(`${base}/api/v1/quote`);
|
||||
url.searchParams.set('chainId', String(chainIdParam));
|
||||
url.searchParams.set('tokenIn', tokenIn);
|
||||
url.searchParams.set('tokenOut', tokenOut);
|
||||
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) {
|
||||
return snap.request({
|
||||
method: 'snap_dialog',
|
||||
params: {
|
||||
type: 'alert',
|
||||
content: (
|
||||
<Box>
|
||||
<Text>
|
||||
<Bold>Swap quote</Bold>
|
||||
</Text>
|
||||
<Text>{data.error ?? `HTTP ${res.status}`}</Text>
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
const out = data.amountOut ?? '0';
|
||||
return snap.request({
|
||||
method: 'snap_dialog',
|
||||
params: {
|
||||
type: 'alert',
|
||||
content: (
|
||||
<Box>
|
||||
<Text>
|
||||
<Bold>Swap quote</Bold>
|
||||
</Text>
|
||||
<Text>
|
||||
In: {amountIn} (raw) → Out: ~{out} (raw)
|
||||
</Text>
|
||||
<Text>Use explorer or dApp to execute swap.</Text>
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return snap.request({
|
||||
method: 'snap_dialog',
|
||||
params: {
|
||||
type: 'alert',
|
||||
content: (
|
||||
<Box>
|
||||
<Text>
|
||||
Failed to fetch quote:{' '}
|
||||
{error instanceof Error ? error.message : 'Unknown error'}
|
||||
</Text>
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
case 'get_bridge_routes': {
|
||||
const bridgeUrl = bridgeListUrl || (base ? `${base}/api/v1/bridge/routes` : '');
|
||||
if (!bridgeUrl) {
|
||||
return {
|
||||
error: 'Pass apiBaseUrl or bridgeListUrl to fetch bridge routes',
|
||||
routes: undefined,
|
||||
chain138Bridges: undefined,
|
||||
};
|
||||
}
|
||||
try {
|
||||
const res = await fetch(bridgeUrl);
|
||||
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 {
|
||||
routes: data.routes,
|
||||
chain138Bridges: data.chain138Bridges,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
error:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Failed to fetch bridge routes',
|
||||
routes: undefined,
|
||||
chain138Bridges: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
case 'show_bridge_routes': {
|
||||
const showBridgeUrl = bridgeListUrl || (base ? `${base}/api/v1/bridge/routes` : '');
|
||||
if (!showBridgeUrl) {
|
||||
return snap.request({
|
||||
method: 'snap_dialog',
|
||||
params: {
|
||||
type: 'alert',
|
||||
content: (
|
||||
<Box>
|
||||
<Text>
|
||||
Pass apiBaseUrl or bridgeListUrl to see bridge routes (CCIP WETH9 / WETH10).
|
||||
</Text>
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
try {
|
||||
const res = await fetch(showBridgeUrl);
|
||||
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>;
|
||||
};
|
||||
const lines: string[] = [];
|
||||
if (data.chain138Bridges) {
|
||||
lines.push(
|
||||
`WETH9 Bridge (138): ${String(data.chain138Bridges.weth9 ?? '').slice(0, 10)}...`,
|
||||
);
|
||||
lines.push(
|
||||
`WETH10 Bridge (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('');
|
||||
lines.push('Use the explorer to execute bridge transfers.');
|
||||
return snap.request({
|
||||
method: 'snap_dialog',
|
||||
params: {
|
||||
type: 'alert',
|
||||
content: (
|
||||
<Box>
|
||||
<Text>
|
||||
<Bold>CCIP Bridge Routes</Bold>
|
||||
</Text>
|
||||
<Text>{lines.join('\n')}</Text>
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return snap.request({
|
||||
method: 'snap_dialog',
|
||||
params: {
|
||||
type: 'alert',
|
||||
content: (
|
||||
<Box>
|
||||
<Text>
|
||||
Failed to fetch bridge routes:{' '}
|
||||
{error instanceof Error ? error.message : 'Unknown error'}
|
||||
</Text>
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
case 'show_market_data': {
|
||||
if (!base) {
|
||||
return snap.request({
|
||||
method: 'snap_dialog',
|
||||
params: {
|
||||
type: 'alert',
|
||||
content: (
|
||||
<Box>
|
||||
<Text>
|
||||
Pass apiBaseUrl to see market data (prices and tokens).
|
||||
</Text>
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
const chainIdParam = (params?.chainId as number | undefined) ?? 138;
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${base}/api/v1/tokens?chainId=${chainIdParam}&limit=20`,
|
||||
);
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}`);
|
||||
}
|
||||
const data = (await res.json()) as {
|
||||
tokens?: Array<{
|
||||
symbol?: string;
|
||||
name?: string;
|
||||
market?: { priceUsd?: number };
|
||||
}>;
|
||||
};
|
||||
const tokens = data.tokens ?? [];
|
||||
const lines =
|
||||
tokens.length === 0
|
||||
? 'No tokens with market data.'
|
||||
: tokens
|
||||
.map((t) => {
|
||||
const price =
|
||||
t.market?.priceUsd != null
|
||||
? `$${Number(t.market.priceUsd).toFixed(4)}`
|
||||
: '—';
|
||||
return `${t.symbol ?? t.name ?? 'Token'}: ${price}`;
|
||||
})
|
||||
.join('\n');
|
||||
return snap.request({
|
||||
method: 'snap_dialog',
|
||||
params: {
|
||||
type: 'alert',
|
||||
content: (
|
||||
<Box>
|
||||
<Text>
|
||||
<Bold>Market data (Chain {String(chainIdParam)})</Bold>
|
||||
</Text>
|
||||
<Text>{lines}</Text>
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return snap.request({
|
||||
method: 'snap_dialog',
|
||||
params: {
|
||||
type: 'alert',
|
||||
content: (
|
||||
<Box>
|
||||
<Text>
|
||||
Failed to fetch market data:{' '}
|
||||
{error instanceof Error ? error.message : 'Unknown error'}
|
||||
</Text>
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
case 'hello':
|
||||
return snap.request({
|
||||
method: 'snap_dialog',
|
||||
params: {
|
||||
type: 'confirmation',
|
||||
content: (
|
||||
<Box>
|
||||
<Text>
|
||||
Hello, <Bold>{origin}</Bold>!
|
||||
</Text>
|
||||
<Text>Chain 138 (DeFi Oracle Meta Mainnet) Snap.</Text>
|
||||
<Text>
|
||||
Use get_networks or get_chain138_config for
|
||||
wallet_addEthereumChain.
|
||||
</Text>
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
default:
|
||||
throw new Error('Method not found.');
|
||||
}
|
||||
};
|
||||
9
chain138-snap/packages/snap/tsconfig.json
Normal file
9
chain138-snap/packages/snap/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "@metamask/snaps-sdk"
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx"]
|
||||
}
|
||||
27
chain138-snap/playwright.config.ts
Normal file
27
chain138-snap/playwright.config.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* E2E tests for the Chain 138 Snap companion site.
|
||||
* Does not drive MetaMask Flask; for full install/connect flow use the manual checklist in TESTING_INSTRUCTIONS.md.
|
||||
* Run: pnpm run test:e2e
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: 1,
|
||||
reporter: 'html',
|
||||
use: {
|
||||
baseURL: 'http://localhost:8000',
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
|
||||
webServer: {
|
||||
command: 'pnpm run start',
|
||||
url: 'http://localhost:8000',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 240_000,
|
||||
},
|
||||
timeout: 60_000,
|
||||
});
|
||||
3
chain138-snap/pnpm-workspace.yaml
Normal file
3
chain138-snap/pnpm-workspace.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
# pnpm workspace (default package manager). Yarn is also supported.
|
||||
packages:
|
||||
- 'packages/*'
|
||||
12
chain138-snap/renovate.json
Normal file
12
chain138-snap/renovate.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:recommended"],
|
||||
"packageRules": [
|
||||
{
|
||||
"matchPackagePatterns": ["^@metamask/"],
|
||||
"groupName": "MetaMask Snap packages",
|
||||
"schedule": ["before 6am on monday"]
|
||||
}
|
||||
],
|
||||
"description": "Keep @metamask/snaps-sdk and related Snap packages up to date; run CI on dependency updates."
|
||||
}
|
||||
9
chain138-snap/scripts/cleanup.sh
Executable file
9
chain138-snap/scripts/cleanup.sh
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -u
|
||||
set -o pipefail
|
||||
|
||||
rm .github/CODEOWNERS
|
||||
rm .github/workflows/security-code-scanner.yml
|
||||
rm -f scripts/cleanup.sh
|
||||
git commit -am "Clean up undesired MetaMask GitHub files"
|
||||
144
chain138-snap/scripts/deploy-snap-site-to-vmid5000.sh
Executable file
144
chain138-snap/scripts/deploy-snap-site-to-vmid5000.sh
Executable file
@@ -0,0 +1,144 @@
|
||||
#!/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 ""
|
||||
74
chain138-snap/scripts/validate-token-lists.sh
Normal file
74
chain138-snap/scripts/validate-token-lists.sh
Normal file
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env bash
|
||||
# Validate that token/bridge/networks list URLs return valid JSON.
|
||||
# Usage: ./validate-token-lists.sh [URL1] [URL2] ...
|
||||
# If no URLs given, reads from env: TOKEN_LIST_JSON_URL, BRIDGE_LIST_JSON_URL, NETWORKS_JSON_URL.
|
||||
# Requires: curl, jq (or python3 for fallback).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
VALID=0
|
||||
INVALID=0
|
||||
|
||||
validate_url() {
|
||||
local url="$1"
|
||||
local name="${2:-$url}"
|
||||
local body
|
||||
if ! body="$(curl -sS -L --connect-timeout 15 --max-time 30 "$url" 2>/dev/null)"; then
|
||||
echo "❌ $name: failed to fetch"
|
||||
((INVALID++)) || true
|
||||
return 1
|
||||
fi
|
||||
if [ -z "$body" ]; then
|
||||
echo "❌ $name: empty response"
|
||||
((INVALID++)) || true
|
||||
return 1
|
||||
fi
|
||||
if command -v jq &>/dev/null; then
|
||||
if echo "$body" | jq . &>/dev/null; then
|
||||
echo "✅ $name: valid JSON"
|
||||
((VALID++)) || true
|
||||
return 0
|
||||
fi
|
||||
else
|
||||
if echo "$body" | python3 -c "import sys,json; json.load(sys.stdin)" 2>/dev/null; then
|
||||
echo "✅ $name: valid JSON"
|
||||
((VALID++)) || true
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
echo "❌ $name: invalid JSON"
|
||||
((INVALID++)) || true
|
||||
return 1
|
||||
}
|
||||
|
||||
echo "=============================================="
|
||||
echo "Token / bridge / networks list JSON validation"
|
||||
echo "=============================================="
|
||||
echo ""
|
||||
|
||||
if [ $# -gt 0 ]; then
|
||||
for url in "$@"; do
|
||||
validate_url "$url" "$url"
|
||||
done
|
||||
else
|
||||
for var in TOKEN_LIST_JSON_URL BRIDGE_LIST_JSON_URL NETWORKS_JSON_URL; do
|
||||
url="${!var:-}"
|
||||
if [ -n "$url" ]; then
|
||||
validate_url "$url" "$var"
|
||||
else
|
||||
echo "⏭ $var not set, skipping"
|
||||
fi
|
||||
done
|
||||
if [ "$VALID" -eq 0 ] && [ "$INVALID" -eq 0 ]; then
|
||||
echo "Set TOKEN_LIST_JSON_URL, BRIDGE_LIST_JSON_URL, or NETWORKS_JSON_URL, or pass URLs as arguments."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Result: $VALID valid, $INVALID invalid"
|
||||
if [ "$INVALID" -gt 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
82
chain138-snap/scripts/verify-snap-site-vmid5000.sh
Executable file
82
chain138-snap/scripts/verify-snap-site-vmid5000.sh
Executable file
@@ -0,0 +1,82 @@
|
||||
#!/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
|
||||
16
chain138-snap/tsconfig.json
Normal file
16
chain138-snap/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"esModuleInterop": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"lib": ["ES2020", "DOM"],
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "node",
|
||||
"noEmit": true,
|
||||
"noErrorTruncation": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"target": "es2020"
|
||||
}
|
||||
}
|
||||
19930
chain138-snap/yarn.lock
Normal file
19930
chain138-snap/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user