mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-06-21 07:31:04 +00:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 920ddbf065 | |||
| a9786b2e70 | |||
| d6ace14e70 | |||
| 2cbfd21539 | |||
| f7c070e45b | |||
| 06ed9734c6 | |||
| d93d4c6cd0 | |||
| faba2dee5d | |||
| ca3976fe62 | |||
| 7fea37eb5d | |||
| 68379dcf3a | |||
| 56f276af1b | |||
| f024d21cf5 | |||
| 67bc28cff4 | |||
| d7fa9dd570 | |||
| 428b058cbc | |||
| 0b5e318538 | |||
| 568507ab98 |
@@ -0,0 +1,41 @@
|
||||
# pwap/web Docker build context (root) — exclude everything not needed
|
||||
# for `web/` build. Other monorepo subprojects stay out of the image.
|
||||
|
||||
# Other monorepo dirs (we only need web/ + shared/)
|
||||
exchange/
|
||||
mobile/
|
||||
pwap-mobile/
|
||||
docs/
|
||||
res/
|
||||
|
||||
# All node_modules everywhere
|
||||
**/node_modules/
|
||||
**/dist/
|
||||
**/build/
|
||||
|
||||
# Git, GitHub
|
||||
.git/
|
||||
.github/
|
||||
|
||||
# Env files (built-in vars are passed as build-args from CI)
|
||||
**/.env
|
||||
**/.env.*
|
||||
!**/.env.example
|
||||
|
||||
# Editor / OS
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
.DS_Store
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Cache
|
||||
**/.eslintcache
|
||||
**/coverage/
|
||||
|
||||
# Already-built artifacts (we rebuild fresh inside container)
|
||||
web/dist/
|
||||
shared/**/dist/
|
||||
@@ -0,0 +1,58 @@
|
||||
name: CodeQL (SAST)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
pull_request:
|
||||
branches: [main, develop]
|
||||
schedule:
|
||||
# Every Sunday at 02:00 UTC — catches CVEs disclosed during the week
|
||||
- cron: '0 2 * * 0'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
actions: read
|
||||
|
||||
concurrency:
|
||||
group: codeql-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze ${{ matrix.language }}
|
||||
runs-on: pwap-runner
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [javascript-typescript]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# OWASP top-10 + injection + auth flaws + prototype pollution
|
||||
queries: security-extended,security-and-quality
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
- name: Perform analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: /language:${{ matrix.language }}
|
||||
# GitHub Advanced Security dashboard upload requires paid plan.
|
||||
# SARIF saved as a downloadable artifact instead.
|
||||
upload: false
|
||||
output: /tmp/codeql-results
|
||||
|
||||
- name: Upload SARIF as artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: codeql-sarif-${{ matrix.language }}
|
||||
path: /tmp/codeql-results/*.sarif
|
||||
retention-days: 7
|
||||
@@ -6,14 +6,25 @@ on:
|
||||
pull_request:
|
||||
branches: [ main, develop ]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
rollback_to:
|
||||
description: 'Rollback to git SHA (skips build, redeploys old image). Empty = normal deploy.'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: write # version bump commit
|
||||
packages: write # GHCR push
|
||||
|
||||
env:
|
||||
VITE_SUPABASE_URL: ${{ secrets.VITE_SUPABASE_URL }}
|
||||
VITE_SUPABASE_ANON_KEY: ${{ secrets.VITE_SUPABASE_ANON_KEY }}
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: pezkuwichain/pwap-web
|
||||
|
||||
jobs:
|
||||
# ========================================
|
||||
@@ -21,7 +32,7 @@ jobs:
|
||||
# ========================================
|
||||
web:
|
||||
name: Web App
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: pwap-runner
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -75,13 +86,164 @@ jobs:
|
||||
path: web/dist/
|
||||
|
||||
# ========================================
|
||||
# DEPLOY WEB APP TO VPS
|
||||
# BUILD & PUSH DOCKER IMAGE TO GHCR
|
||||
# Immutable artifact for audit + rollback (vs ephemeral GHA artifact).
|
||||
# Tagged with git SHA so any commit can be redeployed by SHA.
|
||||
# ========================================
|
||||
deploy:
|
||||
name: Deploy Web
|
||||
runs-on: ubuntu-latest
|
||||
build-image:
|
||||
name: Build & Push Image
|
||||
runs-on: pwap-runner
|
||||
needs: [web, telegram-gate]
|
||||
if: |
|
||||
github.ref == 'refs/heads/main' &&
|
||||
(github.event_name == 'push' ||
|
||||
(github.event_name == 'workflow_dispatch' && github.event.inputs.rollback_to == ''))
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write # cosign keyless signing via Sigstore OIDC
|
||||
outputs:
|
||||
image_sha: ${{ steps.meta.outputs.image_sha }}
|
||||
image_digest: ${{ steps.build.outputs.digest }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Install cosign
|
||||
uses: sigstore/cosign-installer@v3
|
||||
with:
|
||||
cosign-release: 'v2.4.1'
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract image metadata
|
||||
id: meta
|
||||
run: |
|
||||
SHORT_SHA="${GITHUB_SHA:0:7}"
|
||||
echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT
|
||||
echo "image_sha=$SHORT_SHA" >> $GITHUB_OUTPUT
|
||||
echo "image=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and push
|
||||
id: build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./
|
||||
file: ./web/Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
${{ steps.meta.outputs.image }}:${{ steps.meta.outputs.short_sha }}
|
||||
${{ steps.meta.outputs.image }}:latest
|
||||
build-args: |
|
||||
VITE_NETWORK=MAINNET
|
||||
VITE_WS_ENDPOINT=wss://rpc.pezkuwichain.io
|
||||
VITE_WS_ENDPOINT_FALLBACK_1=wss://mainnet.pezkuwichain.io
|
||||
VITE_ASSET_HUB_ENDPOINT=wss://asset-hub-rpc.pezkuwichain.io
|
||||
VITE_PEOPLE_CHAIN_ENDPOINT=wss://people-rpc.pezkuwichain.io
|
||||
VITE_WALLETCONNECT_PROJECT_ID=8292a793b7640e8364c378e331e76d04
|
||||
VITE_SUPABASE_URL=${{ secrets.VITE_SUPABASE_URL }}
|
||||
VITE_SUPABASE_ANON_KEY=${{ secrets.VITE_SUPABASE_ANON_KEY }}
|
||||
cache-from: type=registry,ref=${{ steps.meta.outputs.image }}:cache
|
||||
cache-to: type=registry,ref=${{ steps.meta.outputs.image }}:cache,mode=max
|
||||
provenance: false
|
||||
|
||||
- name: Sign image with cosign (keyless, Sigstore Fulcio)
|
||||
env:
|
||||
COSIGN_EXPERIMENTAL: '1'
|
||||
run: |
|
||||
IMAGE_DIGEST="${{ steps.meta.outputs.image }}@${{ steps.build.outputs.digest }}"
|
||||
# cosign needs its own registry auth — docker/login-action only writes
|
||||
# ~/.docker/config.json which cosign on self-hosted runner can't read
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | cosign login ghcr.io -u "${{ github.actor }}" --password-stdin
|
||||
echo "Signing $IMAGE_DIGEST"
|
||||
cosign sign --yes "$IMAGE_DIGEST"
|
||||
echo "✅ Image signed (transparency log: rekor.sigstore.dev)"
|
||||
|
||||
# ========================================
|
||||
# TELEGRAM CEO APPROVAL GATE
|
||||
# Runs on self-hosted pwap-runner (DEV VPS) where pexsec-bot.service
|
||||
# writes the gate file to /tmp/pexsec-gates/<sha> when CEO clicks
|
||||
# Approve/Cancel in Telegram. 30-minute timeout = deploy cancelled.
|
||||
# ========================================
|
||||
telegram-gate:
|
||||
name: Telegram deploy approval
|
||||
runs-on: pwap-runner
|
||||
needs: [web, security-audit]
|
||||
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
|
||||
|
||||
steps:
|
||||
- name: Send approval request and wait
|
||||
env:
|
||||
BOT_TOKEN: ${{ secrets.PEXSEC_BOT_TOKEN }}
|
||||
CEO_CHAT_ID: ${{ secrets.TELEGRAM_CEO_CHAT_ID }}
|
||||
SHA: ${{ github.sha }}
|
||||
ACTOR: ${{ github.actor }}
|
||||
MESSAGE: ${{ github.event.head_commit.message }}
|
||||
run: |
|
||||
SHORT="${SHA:0:7}"
|
||||
GATE_DIR="/tmp/pexsec-gates"
|
||||
mkdir -p "$GATE_DIR" 2>/dev/null || true
|
||||
rm -f "$GATE_DIR/$SHORT" 2>/dev/null || true
|
||||
|
||||
# Strip Markdown special chars to prevent Telegram parse errors
|
||||
SAFE_MSG=$(echo "${MESSAGE}" | head -1 | tr -d '_*`[]()#|{}!' | cut -c1-120)
|
||||
|
||||
curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"chat_id\": \"${CEO_CHAT_ID}\",
|
||||
\"parse_mode\": \"Markdown\",
|
||||
\"text\": \"🚀 *pwap/web Deploy Approval*\\n\\n\`${SHORT}\` — ${ACTOR}\\n\\n_${SAFE_MSG}_\\n\\nTargets: app.pezkuwichain.io + pex.mom\",
|
||||
\"reply_markup\": {
|
||||
\"inline_keyboard\": [[
|
||||
{\"text\": \"✅ Approve\", \"callback_data\": \"deploy_approve:${SHORT}\"},
|
||||
{\"text\": \"❌ Cancel\", \"callback_data\": \"deploy_cancel:${SHORT}\"}
|
||||
]]
|
||||
}
|
||||
}"
|
||||
|
||||
echo "Waiting for Telegram approval (max 30 min)..."
|
||||
TIMEOUT=1800
|
||||
ELAPSED=0
|
||||
while [ $ELAPSED -lt $TIMEOUT ]; do
|
||||
if [ -f "$GATE_DIR/$SHORT" ]; then
|
||||
DECISION=$(cat "$GATE_DIR/$SHORT")
|
||||
rm -f "$GATE_DIR/$SHORT" 2>/dev/null || true
|
||||
if [ "$DECISION" = "approved" ]; then
|
||||
echo "Deploy approved."
|
||||
exit 0
|
||||
else
|
||||
echo "Deploy cancelled."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
sleep 10
|
||||
ELAPSED=$((ELAPSED + 10))
|
||||
done
|
||||
echo "No approval received within 30 minutes — deploy cancelled."
|
||||
exit 1
|
||||
|
||||
# ========================================
|
||||
# VERSION BUMP (RUNS BEFORE BOTH DEPLOYS)
|
||||
# ========================================
|
||||
bump-version:
|
||||
name: Bump Version
|
||||
runs-on: pwap-runner
|
||||
needs: [web, security-audit, telegram-gate, build-image]
|
||||
# Skip on rollback (workflow_dispatch with rollback_to set)
|
||||
if: |
|
||||
github.ref == 'refs/heads/main' &&
|
||||
(github.event_name == 'push' ||
|
||||
(github.event_name == 'workflow_dispatch' && github.event.inputs.rollback_to == ''))
|
||||
outputs:
|
||||
new_version: ${{ steps.bump.outputs.version }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -101,23 +263,95 @@ jobs:
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Bump version
|
||||
id: bump
|
||||
working-directory: ./web
|
||||
run: |
|
||||
npm version patch --no-git-tag-version
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
echo "NEW_VERSION=$VERSION" >> $GITHUB_ENV
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
cd ..
|
||||
git add web/package.json
|
||||
git commit -m "chore(web): bump version to $VERSION [skip ci]" || echo "No version change"
|
||||
git push || echo "Nothing to push"
|
||||
|
||||
- name: Download build artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: web-dist
|
||||
path: dist/
|
||||
# ========================================
|
||||
# DEPLOY TO app.pezkuwichain.io (DEV VPS)
|
||||
# Pulls SHA-tagged image from GHCR, extracts /dist, scp to VPS.
|
||||
# Health check + auto-rollback to .deploy-tag-prev on failure.
|
||||
# ========================================
|
||||
deploy-app:
|
||||
name: Deploy app.pezkuwichain.io
|
||||
runs-on: pwap-runner
|
||||
needs: [telegram-gate, bump-version, build-image]
|
||||
if: |
|
||||
always() &&
|
||||
needs.telegram-gate.result == 'success' &&
|
||||
((github.event_name == 'push' && needs.build-image.result == 'success' && needs.bump-version.result == 'success') ||
|
||||
(github.event_name == 'workflow_dispatch' && github.event.inputs.rollback_to != ''))
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
env:
|
||||
DOMAIN: app.pezkuwichain.io
|
||||
TARGET_PATH: /var/www/subdomains/app
|
||||
|
||||
- name: Deploy to VPS
|
||||
steps:
|
||||
- name: Determine image SHA
|
||||
id: sha
|
||||
run: |
|
||||
if [ -n "${{ github.event.inputs.rollback_to }}" ]; then
|
||||
echo "sha=${{ github.event.inputs.rollback_to }}" >> $GITHUB_OUTPUT
|
||||
echo "Rolling back to: ${{ github.event.inputs.rollback_to }}"
|
||||
else
|
||||
echo "sha=${{ needs.build-image.outputs.image_sha }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Capture currently-live SHA (for auto-rollback)
|
||||
id: prev
|
||||
run: |
|
||||
# /.deploy-sha is written into every deploy; read what's live now
|
||||
PREV=$(curl -sf --max-time 5 "https://${{ env.DOMAIN }}/.deploy-sha" | head -c 40 | tr -dc 'a-f0-9' || echo "")
|
||||
echo "Previous live SHA: ${PREV:-unknown}"
|
||||
echo "prev=$PREV" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install cosign (for verify)
|
||||
uses: sigstore/cosign-installer@v3
|
||||
with:
|
||||
cosign-release: 'v2.4.1'
|
||||
|
||||
- name: Verify image signature (cosign keyless)
|
||||
env:
|
||||
COSIGN_EXPERIMENTAL: '1'
|
||||
run: |
|
||||
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sha.outputs.sha }}"
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | cosign login ghcr.io -u "${{ github.actor }}" --password-stdin
|
||||
echo "Verifying signature for $IMAGE"
|
||||
cosign verify "$IMAGE" \
|
||||
--certificate-identity-regexp "^https://github.com/pezkuwichain/pwap/.github/workflows/quality-gate.yml@" \
|
||||
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
|
||||
> /dev/null
|
||||
echo "✅ Signature valid — image was built by trusted CI"
|
||||
|
||||
- name: Extract /dist from image
|
||||
run: |
|
||||
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sha.outputs.sha }}"
|
||||
docker pull "$IMAGE"
|
||||
CID=$(docker create "$IMAGE")
|
||||
mkdir -p dist
|
||||
docker cp "$CID:/dist/." dist/
|
||||
docker rm "$CID" >/dev/null
|
||||
# Stamp this build's SHA into dist so future deploys can read PREV
|
||||
echo "${{ steps.sha.outputs.sha }}" > dist/.deploy-sha
|
||||
ls -la dist/ | head -10
|
||||
|
||||
- name: Deploy to DEV VPS
|
||||
uses: appleboy/scp-action@v1.0.0
|
||||
with:
|
||||
host: ${{ secrets.VPS_HOST }}
|
||||
@@ -128,35 +362,314 @@ jobs:
|
||||
target: '/var/www/subdomains/app'
|
||||
strip_components: 1
|
||||
|
||||
- name: Post-deploy notification
|
||||
- name: Health check (60s window)
|
||||
id: healthcheck
|
||||
run: |
|
||||
echo "✅ Deployed web app v${{ env.NEW_VERSION }} to app.pezkuwichain.io"
|
||||
for i in 1 2 3 4 5 6; do
|
||||
if curl -sf --max-time 10 "https://${{ env.DOMAIN }}/" >/dev/null; then
|
||||
echo "✅ ${{ env.DOMAIN }} healthy"
|
||||
exit 0
|
||||
fi
|
||||
echo "Attempt $i/6 failed, retrying in 10s..."
|
||||
sleep 10
|
||||
done
|
||||
echo "❌ Health check failed for ${{ env.DOMAIN }}"
|
||||
exit 1
|
||||
|
||||
# ── Automatic rollback: pull PREV SHA image, redeploy, recheck ──
|
||||
- name: Auto-rollback to previous SHA
|
||||
id: rollback
|
||||
if: failure() && steps.healthcheck.conclusion == 'failure' && steps.prev.outputs.prev != ''
|
||||
run: |
|
||||
PREV="${{ steps.prev.outputs.prev }}"
|
||||
echo "🔄 Rolling back ${{ env.DOMAIN }} to $PREV"
|
||||
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:$PREV"
|
||||
docker pull "$IMAGE"
|
||||
CID=$(docker create "$IMAGE")
|
||||
rm -rf dist && mkdir dist
|
||||
docker cp "$CID:/dist/." dist/
|
||||
docker rm "$CID" >/dev/null
|
||||
echo "$PREV" > dist/.deploy-sha
|
||||
echo "rollback_sha=$PREV" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: SCP rollback artifact
|
||||
if: steps.rollback.outcome == 'success'
|
||||
uses: appleboy/scp-action@v1.0.0
|
||||
with:
|
||||
host: ${{ secrets.VPS_HOST }}
|
||||
username: ${{ secrets.VPS_USER }}
|
||||
key: ${{ secrets.VPS_SSH_KEY }}
|
||||
port: ${{ secrets.VPS_SSH_PORT || 2222 }}
|
||||
source: 'dist/*'
|
||||
target: '/var/www/subdomains/app'
|
||||
strip_components: 1
|
||||
|
||||
- name: Re-health-check after rollback
|
||||
if: steps.rollback.outcome == 'success'
|
||||
id: healthcheck_rb
|
||||
run: |
|
||||
for i in 1 2 3 4 5 6; do
|
||||
if curl -sf --max-time 10 "https://${{ env.DOMAIN }}/" >/dev/null; then
|
||||
echo "✅ Rolled back successfully — ${{ env.DOMAIN }} healthy on ${{ steps.rollback.outputs.rollback_sha }}"
|
||||
exit 0
|
||||
fi
|
||||
sleep 10
|
||||
done
|
||||
echo "❌ Rollback also failed!"
|
||||
exit 1
|
||||
|
||||
- name: Post-deploy notification
|
||||
if: success()
|
||||
run: |
|
||||
echo "✅ Deployed image ${{ steps.sha.outputs.sha }} to ${{ env.DOMAIN }}"
|
||||
|
||||
- name: Notify failure (Telegram)
|
||||
if: failure()
|
||||
env:
|
||||
BOT_TOKEN: ${{ secrets.PEXSEC_BOT_TOKEN }}
|
||||
CEO_CHAT_ID: ${{ secrets.TELEGRAM_CEO_CHAT_ID }}
|
||||
NEW_SHA: ${{ steps.sha.outputs.sha }}
|
||||
PREV_SHA: ${{ steps.prev.outputs.prev }}
|
||||
ROLLBACK_OUTCOME: ${{ steps.rollback.outcome }}
|
||||
RECHECK_OUTCOME: ${{ steps.healthcheck_rb.outcome }}
|
||||
run: |
|
||||
if [ "$RECHECK_OUTCOME" = "success" ]; then
|
||||
MSG="⚠️ pwap/web ${{ env.DOMAIN }}: deploy ($NEW_SHA) failed health check, AUTO-ROLLED-BACK to $PREV_SHA. Site healthy."
|
||||
elif [ "$ROLLBACK_OUTCOME" = "success" ]; then
|
||||
MSG="🚨 pwap/web ${{ env.DOMAIN }}: deploy ($NEW_SHA) failed AND rollback to $PREV_SHA also failed. Manual intervention needed."
|
||||
elif [ -z "$PREV_SHA" ]; then
|
||||
MSG="❌ pwap/web ${{ env.DOMAIN }}: deploy ($NEW_SHA) failed. No previous SHA available (first deploy?). Manual rollback: gh workflow run quality-gate.yml -f rollback_to=<sha>"
|
||||
else
|
||||
MSG="❌ pwap/web ${{ env.DOMAIN }}: deploy ($NEW_SHA) failed. Auto-rollback was not attempted. Manual: gh workflow run quality-gate.yml -f rollback_to=$PREV_SHA"
|
||||
fi
|
||||
curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
|
||||
-d "chat_id=${CEO_CHAT_ID}" --data-urlencode "text=$MSG"
|
||||
|
||||
# ========================================
|
||||
# DEPLOY TO pex.mom (VPS3 — geo-redundant mirror)
|
||||
# ========================================
|
||||
deploy-pex:
|
||||
name: Deploy pex.mom
|
||||
runs-on: pwap-runner
|
||||
needs: [telegram-gate, bump-version, build-image]
|
||||
if: |
|
||||
always() &&
|
||||
needs.telegram-gate.result == 'success' &&
|
||||
((github.event_name == 'push' && needs.build-image.result == 'success' && needs.bump-version.result == 'success') ||
|
||||
(github.event_name == 'workflow_dispatch' && github.event.inputs.rollback_to != ''))
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
env:
|
||||
DOMAIN: pex.mom
|
||||
TARGET_PATH: /var/www/pex.mom
|
||||
|
||||
steps:
|
||||
- name: Determine image SHA
|
||||
id: sha
|
||||
run: |
|
||||
if [ -n "${{ github.event.inputs.rollback_to }}" ]; then
|
||||
echo "sha=${{ github.event.inputs.rollback_to }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "sha=${{ needs.build-image.outputs.image_sha }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Capture currently-live SHA (for auto-rollback)
|
||||
id: prev
|
||||
run: |
|
||||
PREV=$(curl -sf --max-time 5 "https://${{ env.DOMAIN }}/.deploy-sha" | head -c 40 | tr -dc 'a-f0-9' || echo "")
|
||||
echo "Previous live SHA: ${PREV:-unknown}"
|
||||
echo "prev=$PREV" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install cosign (for verify)
|
||||
uses: sigstore/cosign-installer@v3
|
||||
with:
|
||||
cosign-release: 'v2.4.1'
|
||||
|
||||
- name: Verify image signature (cosign keyless)
|
||||
env:
|
||||
COSIGN_EXPERIMENTAL: '1'
|
||||
run: |
|
||||
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sha.outputs.sha }}"
|
||||
echo "Verifying signature for $IMAGE"
|
||||
cosign verify "$IMAGE" \
|
||||
--certificate-identity-regexp "^https://github.com/pezkuwichain/pwap/.github/workflows/quality-gate.yml@" \
|
||||
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
|
||||
> /dev/null
|
||||
echo "✅ Signature valid — image was built by trusted CI"
|
||||
|
||||
- name: Extract /dist from image
|
||||
run: |
|
||||
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sha.outputs.sha }}"
|
||||
docker pull "$IMAGE"
|
||||
CID=$(docker create "$IMAGE")
|
||||
mkdir -p dist
|
||||
docker cp "$CID:/dist/." dist/
|
||||
docker rm "$CID" >/dev/null
|
||||
echo "${{ steps.sha.outputs.sha }}" > dist/.deploy-sha
|
||||
|
||||
- name: Deploy to VPS3
|
||||
uses: appleboy/scp-action@v1.0.0
|
||||
with:
|
||||
host: ${{ secrets.VPS_PEX_HOST }}
|
||||
username: ${{ secrets.VPS_PEX_USER }}
|
||||
key: ${{ secrets.VPS_PEX_SSH_KEY }}
|
||||
port: ${{ secrets.VPS_PEX_SSH_PORT || 22 }}
|
||||
source: 'dist/*'
|
||||
target: '/var/www/pex.mom'
|
||||
strip_components: 1
|
||||
|
||||
- name: Health check (60s window)
|
||||
id: healthcheck
|
||||
run: |
|
||||
for i in 1 2 3 4 5 6; do
|
||||
if curl -sf --max-time 10 "https://${{ env.DOMAIN }}/" >/dev/null; then
|
||||
echo "✅ ${{ env.DOMAIN }} healthy"
|
||||
exit 0
|
||||
fi
|
||||
echo "Attempt $i/6 failed, retrying in 10s..."
|
||||
sleep 10
|
||||
done
|
||||
echo "❌ Health check failed for ${{ env.DOMAIN }}"
|
||||
exit 1
|
||||
|
||||
- name: Auto-rollback to previous SHA
|
||||
id: rollback
|
||||
if: failure() && steps.healthcheck.conclusion == 'failure' && steps.prev.outputs.prev != ''
|
||||
run: |
|
||||
PREV="${{ steps.prev.outputs.prev }}"
|
||||
echo "🔄 Rolling back ${{ env.DOMAIN }} to $PREV"
|
||||
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:$PREV"
|
||||
docker pull "$IMAGE"
|
||||
CID=$(docker create "$IMAGE")
|
||||
rm -rf dist && mkdir dist
|
||||
docker cp "$CID:/dist/." dist/
|
||||
docker rm "$CID" >/dev/null
|
||||
echo "$PREV" > dist/.deploy-sha
|
||||
echo "rollback_sha=$PREV" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: SCP rollback artifact
|
||||
if: steps.rollback.outcome == 'success'
|
||||
uses: appleboy/scp-action@v1.0.0
|
||||
with:
|
||||
host: ${{ secrets.VPS_PEX_HOST }}
|
||||
username: ${{ secrets.VPS_PEX_USER }}
|
||||
key: ${{ secrets.VPS_PEX_SSH_KEY }}
|
||||
port: ${{ secrets.VPS_PEX_SSH_PORT || 22 }}
|
||||
source: 'dist/*'
|
||||
target: '/var/www/pex.mom'
|
||||
strip_components: 1
|
||||
|
||||
- name: Re-health-check after rollback
|
||||
if: steps.rollback.outcome == 'success'
|
||||
id: healthcheck_rb
|
||||
run: |
|
||||
for i in 1 2 3 4 5 6; do
|
||||
if curl -sf --max-time 10 "https://${{ env.DOMAIN }}/" >/dev/null; then
|
||||
echo "✅ Rolled back successfully — ${{ env.DOMAIN }} healthy on ${{ steps.rollback.outputs.rollback_sha }}"
|
||||
exit 0
|
||||
fi
|
||||
sleep 10
|
||||
done
|
||||
echo "❌ Rollback also failed!"
|
||||
exit 1
|
||||
|
||||
- name: Post-deploy notification
|
||||
if: success()
|
||||
run: |
|
||||
echo "✅ Deployed image ${{ steps.sha.outputs.sha }} to ${{ env.DOMAIN }}"
|
||||
|
||||
- name: Notify failure (Telegram)
|
||||
if: failure()
|
||||
env:
|
||||
BOT_TOKEN: ${{ secrets.PEXSEC_BOT_TOKEN }}
|
||||
CEO_CHAT_ID: ${{ secrets.TELEGRAM_CEO_CHAT_ID }}
|
||||
NEW_SHA: ${{ steps.sha.outputs.sha }}
|
||||
PREV_SHA: ${{ steps.prev.outputs.prev }}
|
||||
ROLLBACK_OUTCOME: ${{ steps.rollback.outcome }}
|
||||
RECHECK_OUTCOME: ${{ steps.healthcheck_rb.outcome }}
|
||||
run: |
|
||||
if [ "$RECHECK_OUTCOME" = "success" ]; then
|
||||
MSG="⚠️ pwap/web ${{ env.DOMAIN }}: deploy ($NEW_SHA) failed health check, AUTO-ROLLED-BACK to $PREV_SHA. Site healthy."
|
||||
elif [ "$ROLLBACK_OUTCOME" = "success" ]; then
|
||||
MSG="🚨 pwap/web ${{ env.DOMAIN }}: deploy ($NEW_SHA) failed AND rollback to $PREV_SHA also failed. Manual intervention needed."
|
||||
elif [ -z "$PREV_SHA" ]; then
|
||||
MSG="❌ pwap/web ${{ env.DOMAIN }}: deploy ($NEW_SHA) failed. No previous SHA available (first deploy?). Manual rollback: gh workflow run quality-gate.yml -f rollback_to=<sha>"
|
||||
else
|
||||
MSG="❌ pwap/web ${{ env.DOMAIN }}: deploy ($NEW_SHA) failed. Auto-rollback was not attempted. Manual: gh workflow run quality-gate.yml -f rollback_to=$PREV_SHA"
|
||||
fi
|
||||
curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
|
||||
-d "chat_id=${CEO_CHAT_ID}" --data-urlencode "text=$MSG"
|
||||
|
||||
# ========================================
|
||||
# SECURITY CHECKS (BLOCKING)
|
||||
# npm audit (high + critical) + TruffleHog secret scan
|
||||
# ========================================
|
||||
security-audit:
|
||||
name: Security Audit
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: pwap-runner
|
||||
needs: [web]
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: ${{ github.event_name == 'pull_request' && 0 || 1 }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Web - npm audit (critical only)
|
||||
- name: Web — npm audit (high + critical)
|
||||
working-directory: ./web
|
||||
run: |
|
||||
npm install
|
||||
npm audit --audit-level=critical
|
||||
npm audit --audit-level=high
|
||||
|
||||
- name: TruffleHog Secret Scan
|
||||
- name: TruffleHog — PR diff (verified secrets only)
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: trufflesecurity/trufflehog@main
|
||||
with:
|
||||
base: ${{ github.event.pull_request.base.sha }}
|
||||
head: ${{ github.event.pull_request.head.sha }}
|
||||
extra_args: --only-verified
|
||||
|
||||
- name: TruffleHog — full repo scan (verified secrets only)
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: trufflesecurity/trufflehog@main
|
||||
with:
|
||||
path: ./
|
||||
extra_args: --only-verified
|
||||
|
||||
# ========================================
|
||||
# CI GATE — explicit merge-block
|
||||
# All required checks must succeed (or be skipped, e.g. for rollback path).
|
||||
# Branch protection on main should require this job's success.
|
||||
# ========================================
|
||||
ci-gate:
|
||||
name: CI Gate ✅
|
||||
runs-on: pwap-runner
|
||||
needs: [web, security-audit]
|
||||
if: always()
|
||||
|
||||
steps:
|
||||
- name: Verify all required jobs succeeded or were intentionally skipped
|
||||
run: |
|
||||
results='${{ toJSON(needs) }}'
|
||||
echo "$results" | python3 -c "
|
||||
import json, sys
|
||||
needs = json.load(sys.stdin)
|
||||
failed = [name for name, job in needs.items() if job['result'] not in ('success', 'skipped')]
|
||||
if failed:
|
||||
print('❌ Required jobs failed: ' + ', '.join(failed))
|
||||
sys.exit(1)
|
||||
print('✅ All required CI jobs passed or skipped')
|
||||
"
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
# Internal resources (never commit)
|
||||
res/
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
-2002
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,7 @@ pwap/
|
||||
|
||||
**Status:** ✅ Production Ready
|
||||
|
||||
The primary web interface for Pezkuwi blockchain at [pezkuwichain.app](https://pezkuwichain.app)
|
||||
The primary web interface for Pezkuwi blockchain at [app.pezkuwichain.io](https://app.pezkuwichain.io)
|
||||
|
||||
**Tech Stack:**
|
||||
- React 18 + TypeScript
|
||||
@@ -166,9 +166,10 @@ RTL support for CKB, AR, FA.
|
||||
|
||||
## Links
|
||||
|
||||
- **Website:** https://pezkuwichain.app
|
||||
- **SDK UI:** https://pezkuwichain.app/sdk
|
||||
- **Documentation:** https://docs.pezkuwichain.app
|
||||
- **Website:** https://app.pezkuwichain.io
|
||||
- **Website (alt):** https://pex.mom
|
||||
- **Exchange:** https://pex.network
|
||||
- **Documentation:** https://docs.pezkuwichain.io
|
||||
|
||||
## License
|
||||
|
||||
|
||||
-1
Submodule exchange deleted from bb3bc812ed
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 52 KiB |
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 677 KiB |
@@ -1,31 +0,0 @@
|
||||
# PWAP WSL Dev Environment Setup
|
||||
# Run in PowerShell as Administrator
|
||||
|
||||
Write-Host "=== PWAP Dev Setup ===" -ForegroundColor Cyan
|
||||
|
||||
# 1. Fix .wslconfig - enable mirrored networking
|
||||
$wslconfig = @"
|
||||
[wsl2]
|
||||
memory=48GB
|
||||
swap=16GB
|
||||
networkingMode=mirrored
|
||||
"@
|
||||
Set-Content -Path "$env:USERPROFILE\.wslconfig" -Value $wslconfig
|
||||
Write-Host "[OK] .wslconfig updated (mirrored networking)" -ForegroundColor Green
|
||||
|
||||
# 2. Restart ADB on default port
|
||||
$adbPath = "C:\Users\satos\Desktop\platform-tools\adb.exe"
|
||||
& $adbPath kill-server 2>$null
|
||||
Start-Sleep -Seconds 1
|
||||
& $adbPath start-server
|
||||
Write-Host "[OK] ADB server restarted on port 5037" -ForegroundColor Green
|
||||
& $adbPath devices
|
||||
|
||||
# 3. Shutdown WSL so new config takes effect
|
||||
Write-Host "`nShutting down WSL..." -ForegroundColor Yellow
|
||||
wsl --shutdown
|
||||
Start-Sleep -Seconds 3
|
||||
Write-Host "[OK] WSL shutdown complete" -ForegroundColor Green
|
||||
|
||||
Write-Host "`n=== Done! ===" -ForegroundColor Cyan
|
||||
Write-Host "Now open WSL again and run: cd pwap && claude" -ForegroundColor White
|
||||
@@ -0,0 +1,53 @@
|
||||
# pwap/web — Static SPA build for distribution.
|
||||
# Build context is the pwap repo ROOT (not web/) because vite aliases like
|
||||
# @pezkuwi/utils, @shared/* resolve to ../shared/* — both web/ and shared/
|
||||
# must be in the build context.
|
||||
# Stage 1: build with Node. Stage 2: pure dist/ in busybox (smallest possible
|
||||
# attacker surface — no shell, no package manager, no node runtime).
|
||||
# Tag the resulting image with the git SHA in CI so rollback is just
|
||||
# "pull pwap-web:<old-sha>".
|
||||
|
||||
# ─── Stage 1: Build ────────────────────────────────────────────
|
||||
FROM node:20-alpine AS builder
|
||||
WORKDIR /build/web
|
||||
|
||||
# Copy package files first to leverage Docker layer cache when only src changes
|
||||
COPY web/package.json web/package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
# Copy shared/ first (less frequently changed), then web/ source
|
||||
COPY shared/ /build/shared/
|
||||
COPY web/ /build/web/
|
||||
|
||||
# Build args for environment-specific values (passed from CI)
|
||||
ARG VITE_NETWORK=MAINNET
|
||||
ARG VITE_WS_ENDPOINT=wss://rpc.pezkuwichain.io
|
||||
ARG VITE_WS_ENDPOINT_FALLBACK_1=wss://mainnet.pezkuwichain.io
|
||||
ARG VITE_ASSET_HUB_ENDPOINT=wss://asset-hub-rpc.pezkuwichain.io
|
||||
ARG VITE_PEOPLE_CHAIN_ENDPOINT=wss://people-rpc.pezkuwichain.io
|
||||
ARG VITE_WALLETCONNECT_PROJECT_ID=8292a793b7640e8364c378e331e76d04
|
||||
ARG VITE_SUPABASE_URL
|
||||
ARG VITE_SUPABASE_ANON_KEY
|
||||
|
||||
ENV VITE_NETWORK=$VITE_NETWORK
|
||||
ENV VITE_WS_ENDPOINT=$VITE_WS_ENDPOINT
|
||||
ENV VITE_WS_ENDPOINT_FALLBACK_1=$VITE_WS_ENDPOINT_FALLBACK_1
|
||||
ENV VITE_ASSET_HUB_ENDPOINT=$VITE_ASSET_HUB_ENDPOINT
|
||||
ENV VITE_PEOPLE_CHAIN_ENDPOINT=$VITE_PEOPLE_CHAIN_ENDPOINT
|
||||
ENV VITE_WALLETCONNECT_PROJECT_ID=$VITE_WALLETCONNECT_PROJECT_ID
|
||||
ENV VITE_SUPABASE_URL=$VITE_SUPABASE_URL
|
||||
ENV VITE_SUPABASE_ANON_KEY=$VITE_SUPABASE_ANON_KEY
|
||||
|
||||
RUN npm run build
|
||||
|
||||
# ─── Stage 2: Distribution image ───────────────────────────────
|
||||
# busybox:musl gives us a tiny base (~1.5MB) with a shell for `cp` operations
|
||||
# during deploy extraction, but no npm/curl/wget/ssh — minimal attack surface
|
||||
# if the image were ever exposed.
|
||||
FROM busybox:musl
|
||||
WORKDIR /dist
|
||||
COPY --from=builder /build/web/dist /dist
|
||||
LABEL org.opencontainers.image.source="https://github.com/pezkuwichain/pwap"
|
||||
LABEL org.opencontainers.image.description="pwap/web static SPA — Pezkuwi wallet/exchange frontend"
|
||||
LABEL org.opencontainers.image.licenses="proprietary"
|
||||
CMD ["sh", "-c", "echo 'pwap-web image — extract /dist via: docker create + docker cp'; sleep infinity"]
|
||||
Generated
+297
-268
File diff suppressed because it is too large
Load Diff
+3
-1
@@ -120,7 +120,8 @@
|
||||
"@pezkuwi/x-textdecoder": "^14.0.25",
|
||||
"@pezkuwi/x-textencoder": "^14.0.25",
|
||||
"@pezkuwi/x-ws": "^14.0.25",
|
||||
"@pezkuwi/networks": "^14.0.25"
|
||||
"@pezkuwi/networks": "^14.0.25",
|
||||
"elliptic": "^6.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.9.0",
|
||||
@@ -147,6 +148,7 @@
|
||||
"typescript-eslint": "^8.0.1",
|
||||
"vite": "^7.3.1",
|
||||
"vite-plugin-node-polyfills": "^0.25.0",
|
||||
"vite-plugin-subresource-integrity": "^0.0.12",
|
||||
"vitest": "^4.0.10"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Wallet, TrendingUp, RefreshCw, Award, Plus, Coins, Send, Shield, Users, Fuel, Lock } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ASSET_IDS, getAssetSymbol } from '@pezkuwi/lib/wallet';
|
||||
import { Pez20Badge } from './Pez20Badge';
|
||||
import { AddTokenModal } from './AddTokenModal';
|
||||
import { TransferModal } from './TransferModal';
|
||||
import { XCMTeleportModal } from './XCMTeleportModal';
|
||||
@@ -811,6 +812,7 @@ export const AccountBalance: React.FC = () => {
|
||||
<CardTitle className="text-lg font-medium text-gray-300 whitespace-nowrap">
|
||||
{t('balance.pezBalance')}
|
||||
</CardTitle>
|
||||
<Pez20Badge className="flex-shrink-0" />
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -853,6 +855,7 @@ export const AccountBalance: React.FC = () => {
|
||||
<CardTitle className="text-lg font-medium text-gray-300">
|
||||
{t('balance.usdtBalance')}
|
||||
</CardTitle>
|
||||
<Pez20Badge className="flex-shrink-0" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Small pill marking a token as a Pezkuwi token-standard asset.
|
||||
* PEZ-20 = fungible standard (pallet-assets on Asset Hub), PEZ-721 = NFT standard.
|
||||
* See docs.pezkuwichain.io → Token Standards.
|
||||
*/
|
||||
export const Pez20Badge: React.FC<{ standard?: 'PEZ-20' | 'PEZ-721'; className?: string }> = ({
|
||||
standard = 'PEZ-20',
|
||||
className = '',
|
||||
}) => (
|
||||
<a
|
||||
href="https://docs.pezkuwichain.io/token-standards"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title={`${standard} token standard on Pezkuwi Asset Hub`}
|
||||
className={
|
||||
'inline-flex items-center rounded-full border border-blue-500/40 bg-blue-500/10 ' +
|
||||
'px-2 py-0.5 text-[10px] font-semibold tracking-wide text-blue-300 ' +
|
||||
'hover:bg-blue-500/20 transition-colors no-underline ' +
|
||||
className
|
||||
}
|
||||
>
|
||||
{standard}
|
||||
</a>
|
||||
);
|
||||
|
||||
export default Pez20Badge;
|
||||
@@ -15,6 +15,8 @@ interface ChainStats {
|
||||
validators: number;
|
||||
nominators: number;
|
||||
collators: number;
|
||||
collatorsAH: number;
|
||||
collatorsPeople: number;
|
||||
activeProposals: number;
|
||||
totalVoters: number;
|
||||
citizenCount: number;
|
||||
@@ -325,6 +327,7 @@ const LandingPageDesktop: React.FC = () => {
|
||||
const [stats, setStats] = useState<ChainStats>({
|
||||
latestBlock: 0, finalizedBlock: 0, blockHash: '',
|
||||
peers: 0, validators: 0, nominators: 0, collators: 0,
|
||||
collatorsAH: 0, collatorsPeople: 0,
|
||||
activeProposals: 0, totalVoters: 0, citizenCount: 0,
|
||||
tokensStakedPct: '—',
|
||||
});
|
||||
@@ -417,12 +420,7 @@ const LandingPageDesktop: React.FC = () => {
|
||||
const validators = sessionVals.length;
|
||||
setStats(prev => ({ ...prev, activeProposals, totalVoters, validators }));
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const nomCount = await (api.query.staking as any).counterForNominators?.();
|
||||
if (nomCount != null) setStats(prev => ({ ...prev, nominators: nomCount.toNumber() }));
|
||||
} catch {}
|
||||
// Nominators/staking migrated to Asset Hub — counted in the Asset Hub effect below.
|
||||
})();
|
||||
}, [api, isApiReady]);
|
||||
|
||||
@@ -448,10 +446,18 @@ const LandingPageDesktop: React.FC = () => {
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// Nominators live on Asset Hub after the staking migration (AHM).
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const collCount = await (assetHubApi.query.collatorSelection as any)?.candidates?.();
|
||||
if (collCount != null) setStats(prev => ({ ...prev, collators: collCount.length }));
|
||||
const nomCount = await (assetHubApi.query.staking as any)?.counterForNominators?.();
|
||||
if (nomCount != null) setStats(prev => ({ ...prev, nominators: nomCount.toNumber() }));
|
||||
} catch {}
|
||||
|
||||
// Collators are the invulnerable set (not staking candidates, which are empty).
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const inv = await (assetHubApi.query.collatorSelection as any)?.invulnerables?.();
|
||||
if (inv != null) setStats(prev => ({ ...prev, collatorsAH: inv.length, collators: inv.length + prev.collatorsPeople }));
|
||||
} catch {}
|
||||
})();
|
||||
}, [assetHubApi, isAssetHubReady]);
|
||||
@@ -465,6 +471,13 @@ const LandingPageDesktop: React.FC = () => {
|
||||
const entries = await (peopleApi.query as any).tiki?.citizenNft?.entries?.();
|
||||
if (entries) setStats(prev => ({ ...prev, citizenCount: entries.length }));
|
||||
} catch {}
|
||||
|
||||
// People Chain also runs invulnerable collators — add them to the total.
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const inv = await (peopleApi.query.collatorSelection as any)?.invulnerables?.();
|
||||
if (inv != null) setStats(prev => ({ ...prev, collatorsPeople: inv.length, collators: prev.collatorsAH + inv.length }));
|
||||
} catch {}
|
||||
})();
|
||||
}, [peopleApi, isPeopleReady]);
|
||||
|
||||
|
||||
@@ -165,9 +165,14 @@ export const WalletConnectModal: React.FC<WalletConnectModalProps> = ({ isOpen,
|
||||
<ExternalLink className="mr-2 h-4 w-4" />
|
||||
{t('walletModal.wcOpenApp', 'Open pezWallet')}
|
||||
</Button>
|
||||
<p className="text-xs text-gray-500 text-center">
|
||||
{t('walletModal.wcInstallHint', "Don't have pezWallet? It will be available on Play Store soon.")}
|
||||
</p>
|
||||
<a
|
||||
href="https://play.google.com/store/apps/details?id=io.pezkuwichain.wallet"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs text-gray-500 text-center hover:text-purple-400 transition-colors"
|
||||
>
|
||||
{t('walletModal.wcInstallHint', "Don't have pezWallet? Download it on Play Store.")}
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
// Desktop: QR code
|
||||
|
||||
@@ -40,6 +40,7 @@ export const WalletModal: React.FC<WalletModalProps> = ({ isOpen, onClose }) =>
|
||||
const isMobile = useIsMobile();
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [showWCModal, setShowWCModal] = useState(false);
|
||||
const [isConnecting, setIsConnecting] = useState(false);
|
||||
const [scores, setScores] = useState<UserScores>({
|
||||
trustScore: 0,
|
||||
referralScore: 0,
|
||||
@@ -58,7 +59,12 @@ export const WalletModal: React.FC<WalletModalProps> = ({ isOpen, onClose }) =>
|
||||
};
|
||||
|
||||
const handleConnect = async () => {
|
||||
await connectWallet();
|
||||
setIsConnecting(true);
|
||||
try {
|
||||
await connectWallet();
|
||||
} finally {
|
||||
setIsConnecting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectAccount = (account: typeof accounts[0]) => {
|
||||
@@ -169,6 +175,32 @@ export const WalletModal: React.FC<WalletModalProps> = ({ isOpen, onClose }) =>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Generic Error - any error not caught above */}
|
||||
{error && !error.includes('authorize') && !error.includes('not found') && (
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 bg-red-500/10 border border-red-500/30 rounded-lg">
|
||||
<p className="text-sm text-red-300">{error}</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleConnect}
|
||||
className="w-full bg-gradient-to-r from-purple-600 to-cyan-400 hover:from-purple-700 hover:to-cyan-500"
|
||||
disabled={isConnecting}
|
||||
>
|
||||
{isConnecting ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
{t('walletModal.connectingExtension', 'Approve in extension...')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Wallet className="mr-2 h-4 w-4" />
|
||||
{t('walletModal.tryAgain')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Connected State */}
|
||||
{selectedAccount && !error && (
|
||||
<div className="space-y-4">
|
||||
@@ -351,13 +383,18 @@ export const WalletModal: React.FC<WalletModalProps> = ({ isOpen, onClose }) =>
|
||||
onClick={handleConnect}
|
||||
className="w-full bg-gradient-to-r from-purple-600 to-cyan-400 hover:from-purple-700 hover:to-cyan-500"
|
||||
size="sm"
|
||||
disabled={isApiInitializing}
|
||||
disabled={isApiInitializing || isConnecting}
|
||||
>
|
||||
{isApiInitializing ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
{t('walletModal.connectingBlockchain', 'Connecting to blockchain...')}
|
||||
</>
|
||||
) : isConnecting ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
{t('walletModal.connectingExtension', 'Approve in extension...')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Wallet className="mr-2 h-4 w-4" />
|
||||
@@ -405,9 +442,15 @@ export const WalletModal: React.FC<WalletModalProps> = ({ isOpen, onClose }) =>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<div className="flex items-center justify-center gap-1 text-xs text-gray-400">
|
||||
{t('walletModal.mobileComingSoon')}
|
||||
</div>
|
||||
<a
|
||||
href="https://play.google.com/store/apps/details?id=io.pezkuwichain.wallet"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center gap-1 text-xs text-gray-400 hover:text-purple-400 transition-colors"
|
||||
>
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
{t('walletModal.mobilePlayStore', 'Download on Play Store')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -22,6 +22,14 @@ if (typeof window !== 'undefined' && !import.meta.env.DEV) {
|
||||
import React, { createContext, useContext, useEffect, useState, useRef, ReactNode } from 'react';
|
||||
import { ApiPromise, WsProvider } from '@pezkuwi/api';
|
||||
import { web3Accounts, web3Enable } from '@pezkuwi/extension-dapp';
|
||||
|
||||
const web3EnableWithTimeout = (origin: string, timeoutMs = 20_000): Promise<Awaited<ReturnType<typeof web3Enable>>> =>
|
||||
Promise.race([
|
||||
web3Enable(origin),
|
||||
new Promise<never>((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Extension connection timed out. Please check if the extension popup is blocked by your browser, then click Authorize and try again.')), timeoutMs)
|
||||
)
|
||||
]);
|
||||
import type { InjectedAccountWithMeta } from '@pezkuwi/extension-inject/types';
|
||||
import { DEFAULT_ENDPOINT } from '../../../shared/blockchain/pezkuwi';
|
||||
import { getCurrentNetworkConfig } from '../../../shared/blockchain/endpoints';
|
||||
@@ -371,7 +379,7 @@ export const PezkuwiProvider: React.FC<PezkuwiProviderProps> = ({
|
||||
|
||||
try {
|
||||
// Enable extension (works for both desktop extension and pezWallet DApps browser)
|
||||
const extensions = await web3Enable('PezkuwiChain');
|
||||
const extensions = await web3EnableWithTimeout('PezkuwiChain');
|
||||
if (extensions.length === 0) return;
|
||||
|
||||
// Get accounts
|
||||
@@ -449,7 +457,7 @@ export const PezkuwiProvider: React.FC<PezkuwiProviderProps> = ({
|
||||
// Desktop / pezWallet DApps browser: Try extension (injected provider)
|
||||
const hasExtension = !!(window as unknown as { injectedWeb3?: Record<string, unknown> }).injectedWeb3;
|
||||
|
||||
const extensions = await web3Enable('PezkuwiChain');
|
||||
const extensions = await web3EnableWithTimeout('PezkuwiChain');
|
||||
|
||||
if (extensions.length === 0) {
|
||||
if (hasExtension) {
|
||||
@@ -490,7 +498,7 @@ export const PezkuwiProvider: React.FC<PezkuwiProviderProps> = ({
|
||||
if (import.meta.env.DEV) {
|
||||
console.error('❌ Wallet connection failed:', err);
|
||||
}
|
||||
setError('Failed to connect wallet');
|
||||
setError(err instanceof Error ? err.message : 'Failed to connect wallet');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1840,14 +1840,16 @@ export default {
|
||||
'walletModal.mobileTitle': 'محفظة الجوال',
|
||||
'walletModal.mobileDesc': 'للجوال — امسح رمز QR بتطبيق pezWallet',
|
||||
'walletModal.mobileConnect': 'اتصل بـ pezWallet',
|
||||
'walletModal.mobileComingSoon': 'قريباً على Play Store',
|
||||
'walletModal.mobileComingSoon': 'تنزيل من Play Store',
|
||||
'walletModal.mobilePlayStore': 'تنزيل من Play Store',
|
||||
'walletModal.connectingExtension': 'وافق في الإضافة...',
|
||||
'walletModal.or': 'أو',
|
||||
'walletModal.connectWC': 'اتصل عبر pezWallet (الجوال)',
|
||||
'walletModal.wcScanQR': 'امسح بـ pezWallet للاتصال',
|
||||
'walletModal.wcOpenWallet': 'اتصل بتطبيق pezWallet',
|
||||
'walletModal.wcWaitingMobile': 'وافق على الاتصال في pezWallet',
|
||||
'walletModal.wcOpenApp': 'افتح pezWallet',
|
||||
'walletModal.wcInstallHint': 'ليس لديك pezWallet؟ سيكون متاحاً قريباً على Play Store.',
|
||||
'walletModal.wcInstallHint': 'ليس لديك pezWallet؟ حمّله من Play Store.',
|
||||
'walletModal.wcGenerating': 'جاري إنشاء رمز QR...',
|
||||
'walletModal.wcWaiting': 'في انتظار اتصال المحفظة...',
|
||||
'walletModal.wcConnected': 'تم الاتصال!',
|
||||
|
||||
@@ -1830,14 +1830,16 @@ export default {
|
||||
'walletModal.mobileTitle': 'جزدانی مۆبایل',
|
||||
'walletModal.mobileDesc': 'بۆ مۆبایل — بە pezWallet QR کۆد بخوێنەوە',
|
||||
'walletModal.mobileConnect': 'بە pezWallet پەیوەندی بکە',
|
||||
'walletModal.mobileComingSoon': 'بەم زووانە لە Play Store',
|
||||
'walletModal.mobileComingSoon': 'دابەزاندن لە Play Store',
|
||||
'walletModal.mobilePlayStore': 'دابەزاندن لە Play Store',
|
||||
'walletModal.connectingExtension': 'لە پێوەکراوەکەدا پەسەندی بکە...',
|
||||
'walletModal.or': 'یان',
|
||||
'walletModal.connectWC': 'بە pezWallet پەیوەندی بکە (مۆبایل)',
|
||||
'walletModal.wcScanQR': 'بۆ پەیوەندیکردن بە pezWallet سکان بکە',
|
||||
'walletModal.wcOpenWallet': 'بە pezWallet پەیوەندی بکە',
|
||||
'walletModal.wcWaitingMobile': 'لە pezWallet پەیوەندییەکە پشتڕاست بکەوە',
|
||||
'walletModal.wcOpenApp': 'pezWallet بکەوە',
|
||||
'walletModal.wcInstallHint': 'pezWallet نییە؟ بەم زووانە لە Play Store بەردەست دەبێت.',
|
||||
'walletModal.wcInstallHint': 'pezWallet نییە؟ لە Play Store دابەزێنە.',
|
||||
'walletModal.wcGenerating': 'QR کۆد دروستدەکرێت...',
|
||||
'walletModal.wcWaiting': 'چاوەڕوانی پەیوەندیکردنی جزدان...',
|
||||
'walletModal.wcConnected': 'پەیوەندی کرا!',
|
||||
|
||||
@@ -2199,7 +2199,9 @@ export default {
|
||||
'walletModal.mobileTitle': 'Mobile Wallet',
|
||||
'walletModal.mobileDesc': 'For mobile — scan QR code with pezWallet app',
|
||||
'walletModal.mobileConnect': 'Connect with pezWallet',
|
||||
'walletModal.mobileComingSoon': 'Coming soon on Play Store',
|
||||
'walletModal.mobileComingSoon': 'Download on Play Store',
|
||||
'walletModal.mobilePlayStore': 'Download on Play Store',
|
||||
'walletModal.connectingExtension': 'Approve in extension...',
|
||||
'walletModal.or': 'or',
|
||||
'walletModal.connectWC': 'Connect with pezWallet (Mobile)',
|
||||
'walletModal.wcScanQR': 'Scan with pezWallet to connect',
|
||||
@@ -2208,7 +2210,7 @@ export default {
|
||||
'walletModal.wcWaiting': 'Waiting for wallet to connect...',
|
||||
'walletModal.wcWaitingMobile': 'Approve the connection in pezWallet',
|
||||
'walletModal.wcOpenApp': 'Open pezWallet',
|
||||
'walletModal.wcInstallHint': "Don't have pezWallet? It will be available on Play Store soon.",
|
||||
'walletModal.wcInstallHint': "Don't have pezWallet? Download it on Play Store.",
|
||||
'walletModal.wcConnected': 'Connected!',
|
||||
'walletModal.wcInstructions': 'Open pezWallet app → Settings → WalletConnect → Scan QR code',
|
||||
'walletModal.wcRetry': 'Try Again',
|
||||
|
||||
@@ -1800,14 +1800,16 @@ export default {
|
||||
'walletModal.mobileTitle': 'کیف پول موبایل',
|
||||
'walletModal.mobileDesc': 'برای موبایل — کد QR را با اپلیکیشن pezWallet اسکن کنید',
|
||||
'walletModal.mobileConnect': 'اتصال با pezWallet',
|
||||
'walletModal.mobileComingSoon': 'به زودی در Play Store',
|
||||
'walletModal.mobileComingSoon': 'دانلود از Play Store',
|
||||
'walletModal.mobilePlayStore': 'دانلود از Play Store',
|
||||
'walletModal.connectingExtension': 'در افزونه تأیید کنید...',
|
||||
'walletModal.or': 'یا',
|
||||
'walletModal.connectWC': 'اتصال با pezWallet (موبایل)',
|
||||
'walletModal.wcScanQR': 'برای اتصال با pezWallet اسکن کنید',
|
||||
'walletModal.wcOpenWallet': 'اتصال با اپلیکیشن pezWallet',
|
||||
'walletModal.wcWaitingMobile': 'اتصال را در pezWallet تأیید کنید',
|
||||
'walletModal.wcOpenApp': 'باز کردن pezWallet',
|
||||
'walletModal.wcInstallHint': 'pezWallet ندارید؟ به زودی در Play Store در دسترس خواهد بود.',
|
||||
'walletModal.wcInstallHint': 'pezWallet ندارید؟ از Play Store دانلود کنید.',
|
||||
'walletModal.wcGenerating': 'در حال ایجاد کد QR...',
|
||||
'walletModal.wcWaiting': 'در انتظار اتصال کیف پول...',
|
||||
'walletModal.wcConnected': 'متصل شد!',
|
||||
|
||||
@@ -1857,14 +1857,16 @@ export default {
|
||||
'walletModal.mobileTitle': 'Berîka Mobîl',
|
||||
'walletModal.mobileDesc': 'Ji bo mobîl — bi pezWallet QR kodê bişopîne',
|
||||
'walletModal.mobileConnect': 'Bi pezWallet Ve Girêbide',
|
||||
'walletModal.mobileComingSoon': 'Di nêzîk de li Play Store',
|
||||
'walletModal.mobileComingSoon': 'Ji Play Store dakêşin',
|
||||
'walletModal.mobilePlayStore': 'Ji Play Store dakêşin',
|
||||
'walletModal.connectingExtension': 'Di pêvekê de erê bikin...',
|
||||
'walletModal.or': 'an jî',
|
||||
'walletModal.connectWC': 'Bi pezWallet ve girêbide (Mobîl)',
|
||||
'walletModal.wcScanQR': 'Ji bo girêdanê bi pezWallet re bişopîne',
|
||||
'walletModal.wcOpenWallet': 'Bi pezWallet ve girêbide',
|
||||
'walletModal.wcWaitingMobile': 'Di pezWallet de girêdanê bipejirîne',
|
||||
'walletModal.wcOpenApp': 'pezWallet Veke',
|
||||
'walletModal.wcInstallHint': 'pezWallet tune? Di demek nêzîk de li Play Store dê hebe.',
|
||||
'walletModal.wcInstallHint': 'pezWallet tune? Ji Play Store dakêşin.',
|
||||
'walletModal.wcGenerating': 'QR kod tê çêkirin...',
|
||||
'walletModal.wcWaiting': 'Li benda girêdana berîkê...',
|
||||
'walletModal.wcConnected': 'Girêdayî!',
|
||||
|
||||
@@ -1851,14 +1851,16 @@ export default {
|
||||
'walletModal.mobileTitle': 'Mobil Cüzdan',
|
||||
'walletModal.mobileDesc': 'Mobil için — pezWallet uygulamasıyla QR kodu tarayın',
|
||||
'walletModal.mobileConnect': 'pezWallet ile Bağlan',
|
||||
'walletModal.mobileComingSoon': 'Yakında Play Store\'da',
|
||||
'walletModal.mobileComingSoon': 'Play Store\'dan İndir',
|
||||
'walletModal.mobilePlayStore': 'Play Store\'dan İndir',
|
||||
'walletModal.connectingExtension': 'Uzantıda onaylayın...',
|
||||
'walletModal.or': 'veya',
|
||||
'walletModal.connectWC': 'pezWallet ile Bağlan (Mobil)',
|
||||
'walletModal.wcScanQR': 'Bağlanmak için pezWallet ile tarayın',
|
||||
'walletModal.wcOpenWallet': 'pezWallet uygulamasıyla bağlan',
|
||||
'walletModal.wcWaitingMobile': 'pezWallet\'ta bağlantıyı onaylayın',
|
||||
'walletModal.wcOpenApp': 'pezWallet\'ı Aç',
|
||||
'walletModal.wcInstallHint': 'pezWallet yok mu? Yakında Play Store\'da olacak.',
|
||||
'walletModal.wcInstallHint': 'pezWallet yok mu? Play Store\'dan indirin.',
|
||||
'walletModal.wcGenerating': 'QR kod oluşturuluyor...',
|
||||
'walletModal.wcWaiting': 'Cüzdan bağlantısı bekleniyor...',
|
||||
'walletModal.wcConnected': 'Bağlandı!',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
@@ -12,7 +12,6 @@ const BEREKETLI_API = `${BEREKETLI_URL}/v1`;
|
||||
*/
|
||||
export default function Bereketli() {
|
||||
const { t } = useTranslation();
|
||||
const [error, setError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
@@ -20,8 +19,10 @@ export default function Bereketli() {
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession();
|
||||
// Not signed in: skip SSO and send the user to the Bereketli site,
|
||||
// which handles its own login. Never dead-end on this interstitial.
|
||||
if (!session?.access_token) {
|
||||
setError(t('bereketli.noSession', 'Lütfen önce giriş yapın'));
|
||||
window.location.href = BEREKETLI_URL;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -46,27 +47,14 @@ export default function Bereketli() {
|
||||
});
|
||||
window.location.href = `${BEREKETLI_URL}/app?auth=${btoa(params.toString())}`;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Bağlantı hatası');
|
||||
// SSO failed (expired token, network, etc.) — fall back to the public
|
||||
// Bereketli site instead of stranding the user on app.pezkuwichain.io.
|
||||
if (import.meta.env.DEV) console.warn('Bereketli SSO failed, falling back:', err);
|
||||
window.location.href = BEREKETLI_URL;
|
||||
}
|
||||
})();
|
||||
}, [t]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-950 flex items-center justify-center px-6">
|
||||
<div className="text-center space-y-4">
|
||||
<p className="text-red-400 text-sm">{error}</p>
|
||||
<a
|
||||
href="/"
|
||||
className="inline-block px-4 py-2 bg-green-600 text-white rounded-lg text-sm"
|
||||
>
|
||||
{t('common.backToHome', 'Ana Sayfaya Dön')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-950 flex items-center justify-center">
|
||||
<div className="text-center space-y-3">
|
||||
|
||||
@@ -3,6 +3,7 @@ import { defineConfig } from "vitest/config";
|
||||
import react from "@vitejs/plugin-react-swc";
|
||||
import path from "path";
|
||||
import { nodePolyfills } from 'vite-plugin-node-polyfills';
|
||||
import subresourceIntegrity from 'vite-plugin-subresource-integrity';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(({ command }) => ({
|
||||
@@ -38,6 +39,10 @@ export default defineConfig(({ command }) => ({
|
||||
},
|
||||
protocolImports: true,
|
||||
}),
|
||||
// SRI: production build sırasında <script>/<link> tag'lerine
|
||||
// sha384 integrity hash ekle. CDN/proxy compromise olsa bile
|
||||
// tampered asset browser tarafından load edilmez.
|
||||
command === 'build' ? subresourceIntegrity({ algorithm: 'sha384' }) : null,
|
||||
].filter(Boolean),
|
||||
resolve: {
|
||||
mainFields: ['browser', 'module', 'main', 'exports'],
|
||||
|
||||
Reference in New Issue
Block a user