mirror of
https://github.com/selfxyz/self.git
synced 2026-01-08 22:28:11 -05:00
Merge pull request #1494 from selfxyz/release/staging-2025-12-12
Release to Staging - 2025-12-12
This commit is contained in:
56
.github/actions/generate-github-token/action.yml
vendored
Normal file
56
.github/actions/generate-github-token/action.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
name: "Generate GitHub App Token"
|
||||||
|
description: "Generates a GitHub App token for accessing repositories in the selfxyz organization"
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
app-id:
|
||||||
|
description: "The GitHub App ID"
|
||||||
|
required: true
|
||||||
|
private-key:
|
||||||
|
description: "The GitHub App private key"
|
||||||
|
required: true
|
||||||
|
configure-netrc:
|
||||||
|
description: "If true, writes a ~/.netrc entry for github.com using the generated token (useful for CocoaPods / git HTTPS fetches)"
|
||||||
|
required: false
|
||||||
|
default: "false"
|
||||||
|
netrc-machine:
|
||||||
|
description: "The machine hostname to write into ~/.netrc (default: github.com)"
|
||||||
|
required: false
|
||||||
|
default: "github.com"
|
||||||
|
owner:
|
||||||
|
description: "The owner (organization) of the repositories"
|
||||||
|
required: false
|
||||||
|
default: "selfxyz"
|
||||||
|
repositories:
|
||||||
|
description: "Comma-separated list of repository names to grant access to"
|
||||||
|
required: false
|
||||||
|
default: "NFCPassportReader,android-passport-nfc-reader,react-native-passport-reader,mobile-sdk-native"
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
token:
|
||||||
|
description: "The generated GitHub App installation token"
|
||||||
|
value: ${{ steps.app-token.outputs.token }}
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: "composite"
|
||||||
|
steps:
|
||||||
|
- name: Generate GitHub App Token
|
||||||
|
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
|
||||||
|
id: app-token
|
||||||
|
with:
|
||||||
|
app-id: ${{ inputs.app-id }}
|
||||||
|
private-key: ${{ inputs.private-key }}
|
||||||
|
owner: ${{ inputs.owner }}
|
||||||
|
repositories: ${{ inputs.repositories }}
|
||||||
|
- name: Configure Git auth via ~/.netrc (optional)
|
||||||
|
if: ${{ inputs.configure-netrc == 'true' }}
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
TOKEN="${{ steps.app-token.outputs.token }}"
|
||||||
|
MACHINE="${{ inputs.netrc-machine }}"
|
||||||
|
|
||||||
|
# Mask the token in logs defensively (it shouldn't print, but this protects against future edits).
|
||||||
|
echo "::add-mask::${TOKEN}"
|
||||||
|
|
||||||
|
printf "machine %s\n login x-access-token\n password %s\n" "${MACHINE}" "${TOKEN}" > "${HOME}/.netrc"
|
||||||
|
chmod 600 "${HOME}/.netrc"
|
||||||
21
.github/workflows/mobile-bundle-analysis.yml
vendored
21
.github/workflows/mobile-bundle-analysis.yml
vendored
@@ -5,6 +5,7 @@ env:
|
|||||||
JAVA_VERSION: 17
|
JAVA_VERSION: 17
|
||||||
WORKSPACE: ${{ github.workspace }}
|
WORKSPACE: ${{ github.workspace }}
|
||||||
APP_PATH: ${{ github.workspace }}/app
|
APP_PATH: ${{ github.workspace }}/app
|
||||||
|
NODE_ENV: "production"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
@@ -57,6 +58,14 @@ jobs:
|
|||||||
path: |
|
path: |
|
||||||
~/.gradle/caches
|
~/.gradle/caches
|
||||||
~/.gradle/wrapper
|
~/.gradle/wrapper
|
||||||
|
- name: Generate token for self repositories
|
||||||
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
||||||
|
uses: ./.github/actions/generate-github-token
|
||||||
|
id: github-token
|
||||||
|
with:
|
||||||
|
app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }}
|
||||||
|
private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }}
|
||||||
|
configure-netrc: "true"
|
||||||
- name: Install Mobile Dependencies
|
- name: Install Mobile Dependencies
|
||||||
uses: ./.github/actions/mobile-setup
|
uses: ./.github/actions/mobile-setup
|
||||||
with:
|
with:
|
||||||
@@ -65,7 +74,7 @@ jobs:
|
|||||||
ruby_version: ${{ env.RUBY_VERSION }}
|
ruby_version: ${{ env.RUBY_VERSION }}
|
||||||
workspace: ${{ env.WORKSPACE }}
|
workspace: ${{ env.WORKSPACE }}
|
||||||
env:
|
env:
|
||||||
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
|
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
|
||||||
- name: Build dependencies
|
- name: Build dependencies
|
||||||
shell: bash
|
shell: bash
|
||||||
run: yarn workspace @selfxyz/common build
|
run: yarn workspace @selfxyz/common build
|
||||||
@@ -113,6 +122,14 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: app/ios/Pods
|
path: app/ios/Pods
|
||||||
lockfile: app/ios/Podfile.lock
|
lockfile: app/ios/Podfile.lock
|
||||||
|
- name: Generate token for self repositories
|
||||||
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
||||||
|
uses: ./.github/actions/generate-github-token
|
||||||
|
id: github-token
|
||||||
|
with:
|
||||||
|
app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }}
|
||||||
|
private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }}
|
||||||
|
configure-netrc: "true"
|
||||||
- name: Install Mobile Dependencies
|
- name: Install Mobile Dependencies
|
||||||
uses: ./.github/actions/mobile-setup
|
uses: ./.github/actions/mobile-setup
|
||||||
with:
|
with:
|
||||||
@@ -121,7 +138,7 @@ jobs:
|
|||||||
ruby_version: ${{ env.RUBY_VERSION }}
|
ruby_version: ${{ env.RUBY_VERSION }}
|
||||||
workspace: ${{ env.WORKSPACE }}
|
workspace: ${{ env.WORKSPACE }}
|
||||||
env:
|
env:
|
||||||
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
|
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
|
||||||
- name: Build dependencies
|
- name: Build dependencies
|
||||||
shell: bash
|
shell: bash
|
||||||
run: yarn workspace @selfxyz/common build
|
run: yarn workspace @selfxyz/common build
|
||||||
|
|||||||
29
.github/workflows/mobile-ci.yml
vendored
29
.github/workflows/mobile-ci.yml
vendored
@@ -35,7 +35,7 @@ concurrency:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-deps:
|
build-deps:
|
||||||
runs-on: macos-latest-large
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -90,12 +90,9 @@ jobs:
|
|||||||
- name: Check App Types
|
- name: Check App Types
|
||||||
run: yarn types
|
run: yarn types
|
||||||
working-directory: ./app
|
working-directory: ./app
|
||||||
- name: Check license headers
|
|
||||||
run: node scripts/check-license-headers.mjs --check
|
|
||||||
working-directory: ./
|
|
||||||
|
|
||||||
test:
|
test:
|
||||||
runs-on: macos-latest-large
|
runs-on: ubuntu-latest
|
||||||
needs: build-deps
|
needs: build-deps
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
@@ -190,6 +187,8 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
# Increase Node.js memory to prevent hermes-parser WASM memory errors
|
# Increase Node.js memory to prevent hermes-parser WASM memory errors
|
||||||
NODE_OPTIONS: --max-old-space-size=4096
|
NODE_OPTIONS: --max-old-space-size=4096
|
||||||
|
# Override production NODE_ENV for tests - React's production build doesn't include testing utilities
|
||||||
|
NODE_ENV: test
|
||||||
run: |
|
run: |
|
||||||
# Final verification from app directory perspective
|
# Final verification from app directory perspective
|
||||||
echo "Final verification before running tests (from app directory)..."
|
echo "Final verification before running tests (from app directory)..."
|
||||||
@@ -268,6 +267,7 @@ jobs:
|
|||||||
- name: Cache Ruby gems
|
- name: Cache Ruby gems
|
||||||
uses: ./.github/actions/cache-bundler
|
uses: ./.github/actions/cache-bundler
|
||||||
with:
|
with:
|
||||||
|
# TODO(jcortejoso): Confirm the path of the bundle cache
|
||||||
path: app/ios/vendor/bundle
|
path: app/ios/vendor/bundle
|
||||||
lock-file: app/Gemfile.lock
|
lock-file: app/Gemfile.lock
|
||||||
cache-version: ${{ env.GH_CACHE_VERSION }}-${{ env.GH_GEMS_CACHE_VERSION }}-ruby${{ env.RUBY_VERSION }}
|
cache-version: ${{ env.GH_CACHE_VERSION }}-${{ env.GH_GEMS_CACHE_VERSION }}-ruby${{ env.RUBY_VERSION }}
|
||||||
@@ -315,6 +315,14 @@ jobs:
|
|||||||
bundle config set --local path 'vendor/bundle'
|
bundle config set --local path 'vendor/bundle'
|
||||||
bundle install --jobs 4 --retry 3
|
bundle install --jobs 4 --retry 3
|
||||||
working-directory: ./app
|
working-directory: ./app
|
||||||
|
- name: Generate token for self repositories
|
||||||
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
||||||
|
uses: ./.github/actions/generate-github-token
|
||||||
|
id: github-token
|
||||||
|
with:
|
||||||
|
app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }}
|
||||||
|
private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }}
|
||||||
|
configure-netrc: "true"
|
||||||
- name: Install iOS Dependencies
|
- name: Install iOS Dependencies
|
||||||
uses: nick-fields/retry@v3
|
uses: nick-fields/retry@v3
|
||||||
with:
|
with:
|
||||||
@@ -325,7 +333,7 @@ jobs:
|
|||||||
cd app/ios
|
cd app/ios
|
||||||
bundle exec bash scripts/pod-install-with-cache-fix.sh
|
bundle exec bash scripts/pod-install-with-cache-fix.sh
|
||||||
env:
|
env:
|
||||||
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
|
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
|
||||||
- name: Resolve iOS workspace
|
- name: Resolve iOS workspace
|
||||||
run: |
|
run: |
|
||||||
WORKSPACE_OPEN="ios/OpenPassport.xcworkspace"
|
WORKSPACE_OPEN="ios/OpenPassport.xcworkspace"
|
||||||
@@ -470,12 +478,19 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "Cache miss for built dependencies. Building now..."
|
echo "Cache miss for built dependencies. Building now..."
|
||||||
yarn workspace @selfxyz/mobile-app run build:deps
|
yarn workspace @selfxyz/mobile-app run build:deps
|
||||||
|
- name: Generate token for self repositories
|
||||||
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
||||||
|
uses: ./.github/actions/generate-github-token
|
||||||
|
id: github-token
|
||||||
|
with:
|
||||||
|
app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }}
|
||||||
|
private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }}
|
||||||
- name: Setup Android private modules
|
- name: Setup Android private modules
|
||||||
run: |
|
run: |
|
||||||
cd ${{ env.APP_PATH }}
|
cd ${{ env.APP_PATH }}
|
||||||
PLATFORM=android node scripts/setup-private-modules.cjs
|
PLATFORM=android node scripts/setup-private-modules.cjs
|
||||||
env:
|
env:
|
||||||
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
|
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
|
||||||
CI: true
|
CI: true
|
||||||
- name: Build Android (with AAPT2 symlink fix)
|
- name: Build Android (with AAPT2 symlink fix)
|
||||||
run: yarn android:ci
|
run: yarn android:ci
|
||||||
|
|||||||
26
.github/workflows/mobile-deploy.yml
vendored
26
.github/workflows/mobile-deploy.yml
vendored
@@ -31,6 +31,7 @@ name: Mobile Deploy
|
|||||||
env:
|
env:
|
||||||
# Build environment versions
|
# Build environment versions
|
||||||
RUBY_VERSION: 3.2
|
RUBY_VERSION: 3.2
|
||||||
|
NODE_ENV: "production"
|
||||||
JAVA_VERSION: 17
|
JAVA_VERSION: 17
|
||||||
ANDROID_API_LEVEL: 35
|
ANDROID_API_LEVEL: 35
|
||||||
ANDROID_NDK_VERSION: 27.0.12077973
|
ANDROID_NDK_VERSION: 27.0.12077973
|
||||||
@@ -385,6 +386,7 @@ jobs:
|
|||||||
id: gems-cache
|
id: gems-cache
|
||||||
uses: ./.github/actions/cache-bundler
|
uses: ./.github/actions/cache-bundler
|
||||||
with:
|
with:
|
||||||
|
# TODO(jcortejoso): Confirm the path of the bundle cache
|
||||||
path: ${{ env.APP_PATH }}/ios/vendor/bundle
|
path: ${{ env.APP_PATH }}/ios/vendor/bundle
|
||||||
lock-file: app/Gemfile.lock
|
lock-file: app/Gemfile.lock
|
||||||
cache-version: ${{ env.GH_CACHE_VERSION }}-${{ env.GH_GEMS_CACHE_VERSION }}-ruby${{ env.RUBY_VERSION }}
|
cache-version: ${{ env.GH_CACHE_VERSION }}-${{ env.GH_GEMS_CACHE_VERSION }}-ruby${{ env.RUBY_VERSION }}
|
||||||
@@ -428,6 +430,14 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo "✅ Lock files exist"
|
echo "✅ Lock files exist"
|
||||||
|
- name: Generate token for self repositories
|
||||||
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
||||||
|
uses: ./.github/actions/generate-github-token
|
||||||
|
id: github-token
|
||||||
|
with:
|
||||||
|
app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }}
|
||||||
|
private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }}
|
||||||
|
configure-netrc: "true"
|
||||||
|
|
||||||
- name: Install Mobile Dependencies (main repo)
|
- name: Install Mobile Dependencies (main repo)
|
||||||
if: inputs.platform != 'android' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
if: inputs.platform != 'android' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
||||||
@@ -438,7 +448,7 @@ jobs:
|
|||||||
ruby_version: ${{ env.RUBY_VERSION }}
|
ruby_version: ${{ env.RUBY_VERSION }}
|
||||||
workspace: ${{ env.WORKSPACE }}
|
workspace: ${{ env.WORKSPACE }}
|
||||||
env:
|
env:
|
||||||
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
|
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
|
||||||
|
|
||||||
- name: Install Mobile Dependencies (forked PRs - no secrets)
|
- name: Install Mobile Dependencies (forked PRs - no secrets)
|
||||||
if: inputs.platform != 'android' && github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true
|
if: inputs.platform != 'android' && github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true
|
||||||
@@ -691,7 +701,7 @@ jobs:
|
|||||||
IOS_TESTFLIGHT_GROUPS: ${{ secrets.IOS_TESTFLIGHT_GROUPS }}
|
IOS_TESTFLIGHT_GROUPS: ${{ secrets.IOS_TESTFLIGHT_GROUPS }}
|
||||||
NODE_OPTIONS: "--max-old-space-size=8192"
|
NODE_OPTIONS: "--max-old-space-size=8192"
|
||||||
SEGMENT_KEY: ${{ secrets.SEGMENT_KEY }}
|
SEGMENT_KEY: ${{ secrets.SEGMENT_KEY }}
|
||||||
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
|
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
|
||||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||||
TURNKEY_AUTH_PROXY_CONFIG_ID: ${{ secrets.TURNKEY_AUTH_PROXY_CONFIG_ID }}
|
TURNKEY_AUTH_PROXY_CONFIG_ID: ${{ secrets.TURNKEY_AUTH_PROXY_CONFIG_ID }}
|
||||||
TURNKEY_GOOGLE_CLIENT_ID: ${{ secrets.TURNKEY_GOOGLE_CLIENT_ID }}
|
TURNKEY_GOOGLE_CLIENT_ID: ${{ secrets.TURNKEY_GOOGLE_CLIENT_ID }}
|
||||||
@@ -1046,6 +1056,14 @@ jobs:
|
|||||||
|
|
||||||
echo "✅ Lock files exist"
|
echo "✅ Lock files exist"
|
||||||
|
|
||||||
|
- name: Generate token for self repositories
|
||||||
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
||||||
|
uses: ./.github/actions/generate-github-token
|
||||||
|
id: github-token
|
||||||
|
with:
|
||||||
|
app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }}
|
||||||
|
private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }}
|
||||||
|
|
||||||
- name: Install Mobile Dependencies (main repo)
|
- name: Install Mobile Dependencies (main repo)
|
||||||
if: inputs.platform != 'ios' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
if: inputs.platform != 'ios' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
||||||
uses: ./.github/actions/mobile-setup
|
uses: ./.github/actions/mobile-setup
|
||||||
@@ -1055,7 +1073,7 @@ jobs:
|
|||||||
ruby_version: ${{ env.RUBY_VERSION }}
|
ruby_version: ${{ env.RUBY_VERSION }}
|
||||||
workspace: ${{ env.WORKSPACE }}
|
workspace: ${{ env.WORKSPACE }}
|
||||||
env:
|
env:
|
||||||
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
|
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
|
||||||
PLATFORM: ${{ inputs.platform }}
|
PLATFORM: ${{ inputs.platform }}
|
||||||
|
|
||||||
- name: Install Mobile Dependencies (forked PRs - no secrets)
|
- name: Install Mobile Dependencies (forked PRs - no secrets)
|
||||||
@@ -1112,7 +1130,7 @@ jobs:
|
|||||||
cd ${{ env.APP_PATH }}
|
cd ${{ env.APP_PATH }}
|
||||||
PLATFORM=android node scripts/setup-private-modules.cjs
|
PLATFORM=android node scripts/setup-private-modules.cjs
|
||||||
env:
|
env:
|
||||||
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
|
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
|
||||||
CI: true
|
CI: true
|
||||||
|
|
||||||
- name: Build Dependencies (Android)
|
- name: Build Dependencies (Android)
|
||||||
|
|||||||
30
.github/workflows/mobile-e2e.yml
vendored
30
.github/workflows/mobile-e2e.yml
vendored
@@ -70,6 +70,14 @@ jobs:
|
|||||||
- name: Toggle Yarn hardened mode for trusted PRs
|
- name: Toggle Yarn hardened mode for trusted PRs
|
||||||
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false }}
|
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false }}
|
||||||
run: echo "YARN_ENABLE_HARDENED_MODE=0" >> $GITHUB_ENV
|
run: echo "YARN_ENABLE_HARDENED_MODE=0" >> $GITHUB_ENV
|
||||||
|
- name: Generate token for self repositories
|
||||||
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
||||||
|
uses: ./.github/actions/generate-github-token
|
||||||
|
id: github-token
|
||||||
|
with:
|
||||||
|
app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }}
|
||||||
|
private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }}
|
||||||
|
configure-netrc: "true"
|
||||||
- name: Install deps (internal PRs and protected branches)
|
- name: Install deps (internal PRs and protected branches)
|
||||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
||||||
uses: nick-fields/retry@v3
|
uses: nick-fields/retry@v3
|
||||||
@@ -79,7 +87,7 @@ jobs:
|
|||||||
retry_wait_seconds: 5
|
retry_wait_seconds: 5
|
||||||
command: yarn install --immutable --silent
|
command: yarn install --immutable --silent
|
||||||
env:
|
env:
|
||||||
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
|
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
|
||||||
- name: Install deps (forked PRs - no secrets)
|
- name: Install deps (forked PRs - no secrets)
|
||||||
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
|
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
|
||||||
uses: nick-fields/retry@v3
|
uses: nick-fields/retry@v3
|
||||||
@@ -138,7 +146,7 @@ jobs:
|
|||||||
cd app
|
cd app
|
||||||
PLATFORM=android node scripts/setup-private-modules.cjs
|
PLATFORM=android node scripts/setup-private-modules.cjs
|
||||||
env:
|
env:
|
||||||
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
|
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
|
||||||
CI: true
|
CI: true
|
||||||
- name: Build Android APK
|
- name: Build Android APK
|
||||||
run: |
|
run: |
|
||||||
@@ -149,6 +157,8 @@ jobs:
|
|||||||
- name: Clean up Gradle build artifacts
|
- name: Clean up Gradle build artifacts
|
||||||
uses: ./.github/actions/cleanup-gradle-artifacts
|
uses: ./.github/actions/cleanup-gradle-artifacts
|
||||||
- name: Verify APK and android-passport-nfc-reader integration
|
- name: Verify APK and android-passport-nfc-reader integration
|
||||||
|
env:
|
||||||
|
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
|
||||||
run: |
|
run: |
|
||||||
echo "🔍 Verifying build artifacts..."
|
echo "🔍 Verifying build artifacts..."
|
||||||
APK_PATH="app/android/app/build/outputs/apk/debug/app-debug.apk"
|
APK_PATH="app/android/app/build/outputs/apk/debug/app-debug.apk"
|
||||||
@@ -160,8 +170,8 @@ jobs:
|
|||||||
echo "📱 APK size: $APK_SIZE bytes"
|
echo "📱 APK size: $APK_SIZE bytes"
|
||||||
|
|
||||||
# Verify private modules were properly integrated (skip for forks)
|
# Verify private modules were properly integrated (skip for forks)
|
||||||
if [ -z "${SELFXYZ_INTERNAL_REPO_PAT:-}" ]; then
|
if [ -z "${SELFXYZ_APP_TOKEN:-}" ]; then
|
||||||
echo "🔕 No PAT available — skipping private module verification"
|
echo "🔕 No SELFXYZ_APP_TOKEN available — skipping private module verification"
|
||||||
else
|
else
|
||||||
# Verify android-passport-nfc-reader
|
# Verify android-passport-nfc-reader
|
||||||
if [ -d "app/android/android-passport-nfc-reader" ]; then
|
if [ -d "app/android/android-passport-nfc-reader" ]; then
|
||||||
@@ -263,6 +273,14 @@ jobs:
|
|||||||
- name: Toggle Yarn hardened mode for trusted PRs
|
- name: Toggle Yarn hardened mode for trusted PRs
|
||||||
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false }}
|
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false }}
|
||||||
run: echo "YARN_ENABLE_HARDENED_MODE=0" >> $GITHUB_ENV
|
run: echo "YARN_ENABLE_HARDENED_MODE=0" >> $GITHUB_ENV
|
||||||
|
- name: Generate token for self repositories
|
||||||
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
||||||
|
uses: ./.github/actions/generate-github-token
|
||||||
|
id: github-token
|
||||||
|
with:
|
||||||
|
app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }}
|
||||||
|
private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }}
|
||||||
|
configure-netrc: "true"
|
||||||
- name: Install deps (internal PRs and protected branches)
|
- name: Install deps (internal PRs and protected branches)
|
||||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
||||||
uses: nick-fields/retry@v3
|
uses: nick-fields/retry@v3
|
||||||
@@ -272,7 +290,7 @@ jobs:
|
|||||||
retry_wait_seconds: 5
|
retry_wait_seconds: 5
|
||||||
command: yarn install --immutable --silent
|
command: yarn install --immutable --silent
|
||||||
env:
|
env:
|
||||||
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
|
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
|
||||||
- name: Install deps (forked PRs - no secrets)
|
- name: Install deps (forked PRs - no secrets)
|
||||||
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
|
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
|
||||||
uses: nick-fields/retry@v3
|
uses: nick-fields/retry@v3
|
||||||
@@ -360,7 +378,7 @@ jobs:
|
|||||||
echo "📦 Installing pods via centralized script…"
|
echo "📦 Installing pods via centralized script…"
|
||||||
BUNDLE_GEMFILE=../Gemfile bundle exec bash scripts/pod-install-with-cache-fix.sh
|
BUNDLE_GEMFILE=../Gemfile bundle exec bash scripts/pod-install-with-cache-fix.sh
|
||||||
env:
|
env:
|
||||||
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
|
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
|
||||||
- name: Setup iOS Simulator
|
- name: Setup iOS Simulator
|
||||||
run: |
|
run: |
|
||||||
echo "Setting up iOS Simulator..."
|
echo "Setting up iOS Simulator..."
|
||||||
|
|||||||
36
.github/workflows/mobile-sdk-demo-e2e.yml
vendored
36
.github/workflows/mobile-sdk-demo-e2e.yml
vendored
@@ -59,6 +59,9 @@ jobs:
|
|||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
- run: corepack enable
|
- run: corepack enable
|
||||||
- run: corepack prepare yarn@4.6.0 --activate
|
- run: corepack prepare yarn@4.6.0 --activate
|
||||||
|
- name: Compute .yarnrc.yml hash
|
||||||
|
id: yarnrc-hash
|
||||||
|
uses: ./.github/actions/yarnrc-hash
|
||||||
- name: Cache Yarn dependencies
|
- name: Cache Yarn dependencies
|
||||||
uses: ./.github/actions/cache-yarn
|
uses: ./.github/actions/cache-yarn
|
||||||
with:
|
with:
|
||||||
@@ -66,10 +69,17 @@ jobs:
|
|||||||
.yarn/cache
|
.yarn/cache
|
||||||
.yarn/install-state.gz
|
.yarn/install-state.gz
|
||||||
.yarn/unplugged
|
.yarn/unplugged
|
||||||
cache-version: ${{ env.GH_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}-${{ hashFiles('.yarnrc.yml') }}
|
cache-version: ${{ env.GH_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}-${{ steps.yarnrc-hash.outputs.hash }}
|
||||||
- name: Toggle Yarn hardened mode for trusted PRs
|
- name: Toggle Yarn hardened mode for trusted PRs
|
||||||
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false }}
|
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false }}
|
||||||
run: echo "YARN_ENABLE_HARDENED_MODE=0" >> $GITHUB_ENV
|
run: echo "YARN_ENABLE_HARDENED_MODE=0" >> $GITHUB_ENV
|
||||||
|
- name: Generate token for self repositories
|
||||||
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
||||||
|
uses: ./.github/actions/generate-github-token
|
||||||
|
id: github-token
|
||||||
|
with:
|
||||||
|
app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }}
|
||||||
|
private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }}
|
||||||
- name: Install deps (internal PRs and protected branches)
|
- name: Install deps (internal PRs and protected branches)
|
||||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
||||||
uses: nick-fields/retry@v3
|
uses: nick-fields/retry@v3
|
||||||
@@ -79,7 +89,7 @@ jobs:
|
|||||||
retry_wait_seconds: 5
|
retry_wait_seconds: 5
|
||||||
command: yarn install --immutable --silent
|
command: yarn install --immutable --silent
|
||||||
env:
|
env:
|
||||||
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
|
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
|
||||||
- name: Install deps (forked PRs - no secrets)
|
- name: Install deps (forked PRs - no secrets)
|
||||||
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
|
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
|
||||||
uses: nick-fields/retry@v3
|
uses: nick-fields/retry@v3
|
||||||
@@ -220,6 +230,9 @@ jobs:
|
|||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
- run: corepack enable
|
- run: corepack enable
|
||||||
- run: corepack prepare yarn@4.6.0 --activate
|
- run: corepack prepare yarn@4.6.0 --activate
|
||||||
|
- name: Compute .yarnrc.yml hash
|
||||||
|
id: yarnrc-hash
|
||||||
|
uses: ./.github/actions/yarnrc-hash
|
||||||
- name: Cache Yarn dependencies
|
- name: Cache Yarn dependencies
|
||||||
uses: ./.github/actions/cache-yarn
|
uses: ./.github/actions/cache-yarn
|
||||||
with:
|
with:
|
||||||
@@ -227,10 +240,17 @@ jobs:
|
|||||||
.yarn/cache
|
.yarn/cache
|
||||||
.yarn/install-state.gz
|
.yarn/install-state.gz
|
||||||
.yarn/unplugged
|
.yarn/unplugged
|
||||||
cache-version: ${{ env.GH_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}-${{ hashFiles('.yarnrc.yml') }}
|
cache-version: ${{ env.GH_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}-${{ steps.yarnrc-hash.outputs.hash }}
|
||||||
- name: Toggle Yarn hardened mode for trusted PRs
|
- name: Toggle Yarn hardened mode for trusted PRs
|
||||||
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false }}
|
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false }}
|
||||||
run: echo "YARN_ENABLE_HARDENED_MODE=0" >> $GITHUB_ENV
|
run: echo "YARN_ENABLE_HARDENED_MODE=0" >> $GITHUB_ENV
|
||||||
|
- name: Generate token for self repositories
|
||||||
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
||||||
|
uses: ./.github/actions/generate-github-token
|
||||||
|
id: github-token
|
||||||
|
with:
|
||||||
|
app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }}
|
||||||
|
private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }}
|
||||||
- name: Install deps (internal PRs and protected branches)
|
- name: Install deps (internal PRs and protected branches)
|
||||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
||||||
uses: nick-fields/retry@v3
|
uses: nick-fields/retry@v3
|
||||||
@@ -240,7 +260,7 @@ jobs:
|
|||||||
retry_wait_seconds: 5
|
retry_wait_seconds: 5
|
||||||
command: yarn install --immutable --silent
|
command: yarn install --immutable --silent
|
||||||
env:
|
env:
|
||||||
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
|
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
|
||||||
- name: Install deps (forked PRs - no secrets)
|
- name: Install deps (forked PRs - no secrets)
|
||||||
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
|
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
|
||||||
uses: nick-fields/retry@v3
|
uses: nick-fields/retry@v3
|
||||||
@@ -316,15 +336,15 @@ jobs:
|
|||||||
max_attempts: 3
|
max_attempts: 3
|
||||||
retry_wait_seconds: 10
|
retry_wait_seconds: 10
|
||||||
command: |
|
command: |
|
||||||
if [ -n "${SELFXYZ_INTERNAL_REPO_PAT}" ]; then
|
if [ -n "${SELFXYZ_APP_TOKEN}" ]; then
|
||||||
echo "🔑 Using SELFXYZ_INTERNAL_REPO_PAT for private pod access"
|
echo "🔑 Using GitHub App token for private pod access"
|
||||||
echo "::add-mask::${SELFXYZ_INTERNAL_REPO_PAT}"
|
echo "::add-mask::${SELFXYZ_APP_TOKEN}"
|
||||||
fi
|
fi
|
||||||
cd packages/mobile-sdk-demo/ios
|
cd packages/mobile-sdk-demo/ios
|
||||||
echo "📦 Installing pods via cache-fix script…"
|
echo "📦 Installing pods via cache-fix script…"
|
||||||
bash scripts/pod-install-with-cache-fix.sh
|
bash scripts/pod-install-with-cache-fix.sh
|
||||||
env:
|
env:
|
||||||
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
|
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
|
||||||
GIT_TERMINAL_PROMPT: 0
|
GIT_TERMINAL_PROMPT: 0
|
||||||
- name: Setup iOS Simulator
|
- name: Setup iOS Simulator
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
8
.github/workflows/qrcode-sdk-ci.yml
vendored
8
.github/workflows/qrcode-sdk-ci.yml
vendored
@@ -76,6 +76,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
common/dist
|
common/dist
|
||||||
|
sdk/sdk-common/dist
|
||||||
sdk/qrcode/dist
|
sdk/qrcode/dist
|
||||||
key: qrcode-sdk-build-${{ env.GH_SDK_CACHE_VERSION }}-${{ github.sha }}
|
key: qrcode-sdk-build-${{ env.GH_SDK_CACHE_VERSION }}-${{ github.sha }}
|
||||||
|
|
||||||
@@ -128,6 +129,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
common/dist
|
common/dist
|
||||||
|
sdk/sdk-common/dist
|
||||||
sdk/qrcode/dist
|
sdk/qrcode/dist
|
||||||
key: qrcode-sdk-build-${{ env.GH_SDK_CACHE_VERSION }}-${{ github.sha }}
|
key: qrcode-sdk-build-${{ env.GH_SDK_CACHE_VERSION }}-${{ github.sha }}
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -195,6 +197,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
common/dist
|
common/dist
|
||||||
|
sdk/sdk-common/dist
|
||||||
sdk/qrcode/dist
|
sdk/qrcode/dist
|
||||||
key: qrcode-sdk-build-${{ env.GH_SDK_CACHE_VERSION }}-${{ github.sha }}
|
key: qrcode-sdk-build-${{ env.GH_SDK_CACHE_VERSION }}-${{ github.sha }}
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -203,6 +206,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "Verifying build artifacts..."
|
echo "Verifying build artifacts..."
|
||||||
ls -la common/dist/
|
ls -la common/dist/
|
||||||
|
ls -la sdk/sdk-common/dist/
|
||||||
ls -la sdk/qrcode/dist/
|
ls -la sdk/qrcode/dist/
|
||||||
echo "✅ Build artifacts verified"
|
echo "✅ Build artifacts verified"
|
||||||
|
|
||||||
@@ -255,13 +259,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
common/dist
|
common/dist
|
||||||
|
sdk/sdk-common/dist
|
||||||
sdk/qrcode/dist
|
sdk/qrcode/dist
|
||||||
key: qrcode-sdk-build-${{ env.GH_SDK_CACHE_VERSION }}-${{ github.sha }}
|
key: qrcode-sdk-build-${{ env.GH_SDK_CACHE_VERSION }}-${{ github.sha }}
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
|
|
||||||
- name: Build SDK common dependency
|
|
||||||
run: yarn workspace @selfxyz/sdk-common build
|
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: yarn workspace @selfxyz/qrcode test
|
run: yarn workspace @selfxyz/qrcode test
|
||||||
|
|
||||||
|
|||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -24,3 +24,8 @@ packages/mobile-sdk-alpha/docs/docstrings-report.json
|
|||||||
|
|
||||||
# Private Android modules (cloned at build time)
|
# Private Android modules (cloned at build time)
|
||||||
app/android/android-passport-nfc-reader/
|
app/android/android-passport-nfc-reader/
|
||||||
|
|
||||||
|
# Foundry
|
||||||
|
contracts/out/
|
||||||
|
contracts/cache_forge/
|
||||||
|
contracts/broadcast/
|
||||||
|
|||||||
7
.gitmodules
vendored
7
.gitmodules
vendored
@@ -1,3 +1,10 @@
|
|||||||
|
[submodule "contracts/lib/openzeppelin-foundry-upgrades"]
|
||||||
|
path = contracts/lib/openzeppelin-foundry-upgrades
|
||||||
|
url = https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades
|
||||||
|
[submodule "contracts/lib/forge-std"]
|
||||||
|
path = contracts/lib/forge-std
|
||||||
|
url = https://github.com/foundry-rs/forge-std
|
||||||
|
|
||||||
[submodule "packages/mobile-sdk-alpha/mobile-sdk-native"]
|
[submodule "packages/mobile-sdk-alpha/mobile-sdk-native"]
|
||||||
path = packages/mobile-sdk-alpha/mobile-sdk-native
|
path = packages/mobile-sdk-alpha/mobile-sdk-native
|
||||||
url = git@github.com:selfxyz/mobile-sdk-native.git
|
url = git@github.com:selfxyz/mobile-sdk-native.git
|
||||||
|
|||||||
@@ -245,6 +245,8 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
// Allow any types in tests for mocking
|
// Allow any types in tests for mocking
|
||||||
'@typescript-eslint/no-explicit-any': 'off',
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
// Allow test skipping without warnings
|
||||||
|
'jest/no-disabled-tests': 'off',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ GEM
|
|||||||
artifactory (3.0.17)
|
artifactory (3.0.17)
|
||||||
atomos (0.1.3)
|
atomos (0.1.3)
|
||||||
aws-eventstream (1.4.0)
|
aws-eventstream (1.4.0)
|
||||||
aws-partitions (1.1190.0)
|
aws-partitions (1.1194.0)
|
||||||
aws-sdk-core (3.239.2)
|
aws-sdk-core (3.239.2)
|
||||||
aws-eventstream (~> 1, >= 1.3.0)
|
aws-eventstream (~> 1, >= 1.3.0)
|
||||||
aws-partitions (~> 1, >= 1.992.0)
|
aws-partitions (~> 1, >= 1.992.0)
|
||||||
@@ -86,7 +86,7 @@ GEM
|
|||||||
colored2 (3.1.2)
|
colored2 (3.1.2)
|
||||||
commander (4.6.0)
|
commander (4.6.0)
|
||||||
highline (~> 2.0.0)
|
highline (~> 2.0.0)
|
||||||
concurrent-ruby (1.3.5)
|
concurrent-ruby (1.3.6)
|
||||||
connection_pool (3.0.2)
|
connection_pool (3.0.2)
|
||||||
declarative (0.0.20)
|
declarative (0.0.20)
|
||||||
digest-crc (0.7.0)
|
digest-crc (0.7.0)
|
||||||
@@ -222,14 +222,14 @@ GEM
|
|||||||
i18n (1.14.7)
|
i18n (1.14.7)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
jmespath (1.6.2)
|
jmespath (1.6.2)
|
||||||
json (2.17.1)
|
json (2.18.0)
|
||||||
jwt (2.10.2)
|
jwt (2.10.2)
|
||||||
base64
|
base64
|
||||||
logger (1.7.0)
|
logger (1.7.0)
|
||||||
mini_magick (4.13.2)
|
mini_magick (4.13.2)
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
mini_portile2 (2.8.9)
|
mini_portile2 (2.8.9)
|
||||||
minitest (5.26.2)
|
minitest (5.27.0)
|
||||||
molinillo (0.8.0)
|
molinillo (0.8.0)
|
||||||
multi_json (1.18.0)
|
multi_json (1.18.0)
|
||||||
multipart-post (2.4.1)
|
multipart-post (2.4.1)
|
||||||
@@ -241,7 +241,7 @@ GEM
|
|||||||
nokogiri (1.18.10)
|
nokogiri (1.18.10)
|
||||||
mini_portile2 (~> 2.8.2)
|
mini_portile2 (~> 2.8.2)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
optparse (0.8.0)
|
optparse (0.8.1)
|
||||||
os (1.1.4)
|
os (1.1.4)
|
||||||
plist (3.7.2)
|
plist (3.7.2)
|
||||||
public_suffix (4.0.7)
|
public_suffix (4.0.7)
|
||||||
|
|||||||
@@ -26,4 +26,9 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
env: {
|
||||||
|
production: {
|
||||||
|
plugins: ['transform-remove-console'],
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -42,8 +42,15 @@
|
|||||||
<string></string>
|
<string></string>
|
||||||
<key>LSApplicationQueriesSchemes</key>
|
<key>LSApplicationQueriesSchemes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>whatsapp</string>
|
<string>argent</string>
|
||||||
|
<string>cbwallet</string>
|
||||||
|
<string>coinbase</string>
|
||||||
|
<string>metamask</string>
|
||||||
|
<string>rainbow</string>
|
||||||
<string>sms</string>
|
<string>sms</string>
|
||||||
|
<string>trust</string>
|
||||||
|
<string>wc</string>
|
||||||
|
<string>whatsapp</string>
|
||||||
</array>
|
</array>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ def using_https_git_auth?
|
|||||||
auth_data.include?("Logged in to github.com account") &&
|
auth_data.include?("Logged in to github.com account") &&
|
||||||
auth_data.include?("Git operations protocol: https")
|
auth_data.include?("Git operations protocol: https")
|
||||||
rescue => e
|
rescue => e
|
||||||
puts "gh auth status failed, assuming no HTTPS auth -- will try SSH"
|
# Avoid printing auth-related details in CI logs.
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -51,18 +51,16 @@ target "Self" do
|
|||||||
# External fork - use public NFCPassportReader repository (placeholder)
|
# External fork - use public NFCPassportReader repository (placeholder)
|
||||||
# TODO: Replace with actual public NFCPassportReader repository URL
|
# TODO: Replace with actual public NFCPassportReader repository URL
|
||||||
nfc_repo_url = "https://github.com/PLACEHOLDER/NFCPassportReader.git"
|
nfc_repo_url = "https://github.com/PLACEHOLDER/NFCPassportReader.git"
|
||||||
puts "📦 Using public NFCPassportReader for external fork (#{ENV["GITHUB_REPOSITORY"]})"
|
elsif ENV["GITHUB_ACTIONS"] == "true"
|
||||||
elsif ENV["GITHUB_ACTIONS"] == "true" && ENV["SELFXYZ_INTERNAL_REPO_PAT"]
|
# CI: NEVER embed credentials in URLs. Rely on workflow-provided auth via:
|
||||||
# Running in selfxyz GitHub Actions with PAT available - use private repo with token
|
# - ~/.netrc or a Git credential helper, and token masking in logs.
|
||||||
nfc_repo_url = "https://#{ENV["SELFXYZ_INTERNAL_REPO_PAT"]}@github.com/selfxyz/NFCPassportReader.git"
|
nfc_repo_url = "https://github.com/selfxyz/NFCPassportReader.git"
|
||||||
puts "📦 Using private NFCPassportReader with PAT (selfxyz GitHub Actions)"
|
|
||||||
elsif using_https_git_auth?
|
elsif using_https_git_auth?
|
||||||
# Local development with HTTPS GitHub auth via gh - use HTTPS to private repo
|
# Local development with HTTPS GitHub auth via gh - use HTTPS to private repo
|
||||||
nfc_repo_url = "https://github.com/selfxyz/NFCPassportReader.git"
|
nfc_repo_url = "https://github.com/selfxyz/NFCPassportReader.git"
|
||||||
else
|
else
|
||||||
# Local development in selfxyz repo - use SSH to private repo
|
# Local development in selfxyz repo - use SSH to private repo
|
||||||
nfc_repo_url = "git@github.com:selfxyz/NFCPassportReader.git"
|
nfc_repo_url = "git@github.com:selfxyz/NFCPassportReader.git"
|
||||||
puts "📦 Using SSH for private NFCPassportReader (local selfxyz development)"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
pod "NFCPassportReader", git: nfc_repo_url, commit: "9eff7c4e3a9037fdc1e03301584e0d5dcf14d76b"
|
pod "NFCPassportReader", git: nfc_repo_url, commit: "9eff7c4e3a9037fdc1e03301584e0d5dcf14d76b"
|
||||||
|
|||||||
@@ -2644,6 +2644,6 @@ SPEC CHECKSUMS:
|
|||||||
SwiftyTesseract: 1f3d96668ae92dc2208d9842c8a59bea9fad2cbb
|
SwiftyTesseract: 1f3d96668ae92dc2208d9842c8a59bea9fad2cbb
|
||||||
Yoga: 1259c7a8cbaccf7b4c3ddf8ee36ca11be9dee407
|
Yoga: 1259c7a8cbaccf7b4c3ddf8ee36ca11be9dee407
|
||||||
|
|
||||||
PODFILE CHECKSUM: b5f11f935be22fce84c5395aaa203b50427a79aa
|
PODFILE CHECKSUM: 0aa47f53692543349c43673cda7380fa23049eba
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
COCOAPODS: 1.16.2
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ module.exports = {
|
|||||||
'node',
|
'node',
|
||||||
],
|
],
|
||||||
transformIgnorePatterns: [
|
transformIgnorePatterns: [
|
||||||
'node_modules/(?!(react-native|@react-native|@react-navigation|@react-native-community|@segment/analytics-react-native|@openpassport|react-native-keychain|react-native-check-version|react-native-nfc-manager|react-native-passport-reader|react-native-gesture-handler|uuid|@stablelib|@react-native-google-signin|react-native-cloud-storage|@react-native-clipboard|@react-native-firebase|@selfxyz|@sentry|@anon-aadhaar|react-native-svg|react-native-svg-circle-country-flags)/)',
|
'node_modules/(?!(react-native|@react-native|@react-navigation|@react-native-community|@segment/analytics-react-native|@openpassport|react-native-keychain|react-native-check-version|react-native-nfc-manager|react-native-passport-reader|react-native-gesture-handler|uuid|@stablelib|@react-native-google-signin|react-native-cloud-storage|@react-native-clipboard|@react-native-firebase|@selfxyz|@sentry|@anon-aadhaar|react-native-svg|react-native-svg-circle-country-flags|react-native-blur-effect)/)',
|
||||||
],
|
],
|
||||||
setupFiles: ['<rootDir>/jest.setup.js'],
|
setupFiles: ['<rootDir>/jest.setup.js'],
|
||||||
testMatch: [
|
testMatch: [
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
"sync-versions": "bundle exec fastlane ios sync_version && bundle exec fastlane android sync_version",
|
"sync-versions": "bundle exec fastlane ios sync_version && bundle exec fastlane android sync_version",
|
||||||
"tag:release": "node scripts/tag.cjs release",
|
"tag:release": "node scripts/tag.cjs release",
|
||||||
"tag:remove": "node scripts/tag.cjs remove",
|
"tag:remove": "node scripts/tag.cjs remove",
|
||||||
"test": "yarn build:deps && yarn jest:run --passWithNoTests && node --test scripts/tests/*.cjs",
|
"test": "yarn jest:run --passWithNoTests && node --test scripts/tests/*.cjs",
|
||||||
"test:build": "yarn build:deps && yarn types && node ./scripts/bundle-analyze-ci.cjs ios && yarn test",
|
"test:build": "yarn build:deps && yarn types && node ./scripts/bundle-analyze-ci.cjs ios && yarn test",
|
||||||
"test:ci": "yarn jest:run --passWithNoTests && node --test scripts/tests/*.cjs",
|
"test:ci": "yarn jest:run --passWithNoTests && node --test scripts/tests/*.cjs",
|
||||||
"test:coverage": "yarn jest:run --coverage --passWithNoTests",
|
"test:coverage": "yarn jest:run --coverage --passWithNoTests",
|
||||||
@@ -105,6 +105,7 @@
|
|||||||
"@segment/analytics-react-native": "^2.21.2",
|
"@segment/analytics-react-native": "^2.21.2",
|
||||||
"@segment/sovran-react-native": "^1.1.3",
|
"@segment/sovran-react-native": "^1.1.3",
|
||||||
"@selfxyz/common": "workspace:^",
|
"@selfxyz/common": "workspace:^",
|
||||||
|
"@selfxyz/euclid": "^0.6.0",
|
||||||
"@selfxyz/mobile-sdk-alpha": "workspace:^",
|
"@selfxyz/mobile-sdk-alpha": "workspace:^",
|
||||||
"@sentry/react": "^9.32.0",
|
"@sentry/react": "^9.32.0",
|
||||||
"@sentry/react-native": "7.0.1",
|
"@sentry/react-native": "7.0.1",
|
||||||
@@ -209,6 +210,7 @@
|
|||||||
"@typescript-eslint/parser": "^8.39.0",
|
"@typescript-eslint/parser": "^8.39.0",
|
||||||
"@vitejs/plugin-react-swc": "^3.10.2",
|
"@vitejs/plugin-react-swc": "^3.10.2",
|
||||||
"babel-plugin-module-resolver": "^5.0.2",
|
"babel-plugin-module-resolver": "^5.0.2",
|
||||||
|
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||||
"constants-browserify": "^1.0.0",
|
"constants-browserify": "^1.0.0",
|
||||||
"dompurify": "^3.2.6",
|
"dompurify": "^3.2.6",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ if (!platform || !['android', 'ios'].includes(platform)) {
|
|||||||
// Bundle size thresholds in MB - easy to update!
|
// Bundle size thresholds in MB - easy to update!
|
||||||
const BUNDLE_THRESHOLDS_MB = {
|
const BUNDLE_THRESHOLDS_MB = {
|
||||||
// TODO: fix temporary bundle bump
|
// TODO: fix temporary bundle bump
|
||||||
ios: 44,
|
ios: 45,
|
||||||
android: 44,
|
android: 45,
|
||||||
};
|
};
|
||||||
|
|
||||||
function formatBytes(bytes) {
|
function formatBytes(bytes) {
|
||||||
|
|||||||
@@ -109,7 +109,13 @@ clone_private_module() {
|
|||||||
local dir_name=$(basename "$target_dir")
|
local dir_name=$(basename "$target_dir")
|
||||||
|
|
||||||
# Use different clone methods based on environment
|
# Use different clone methods based on environment
|
||||||
if is_ci && [[ -n "${SELFXYZ_INTERNAL_REPO_PAT:-}" ]]; then
|
if is_ci && [[ -n "${SELFXYZ_APP_TOKEN:-}" ]]; then
|
||||||
|
# CI environment with GitHub App installation token
|
||||||
|
git clone "https://x-access-token:${SELFXYZ_APP_TOKEN}@github.com/selfxyz/${repo_name}.git" "$dir_name" || {
|
||||||
|
log "ERROR: Failed to clone $repo_name with GitHub App token"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
elif is_ci && [[ -n "${SELFXYZ_INTERNAL_REPO_PAT:-}" ]]; then
|
||||||
# CI environment with PAT (fallback if action didn't run)
|
# CI environment with PAT (fallback if action didn't run)
|
||||||
git clone "https://${SELFXYZ_INTERNAL_REPO_PAT}@github.com/selfxyz/${repo_name}.git" "$dir_name" || {
|
git clone "https://${SELFXYZ_INTERNAL_REPO_PAT}@github.com/selfxyz/${repo_name}.git" "$dir_name" || {
|
||||||
log "ERROR: Failed to clone $repo_name with PAT"
|
log "ERROR: Failed to clone $repo_name with PAT"
|
||||||
@@ -119,14 +125,14 @@ clone_private_module() {
|
|||||||
# Local development with SSH
|
# Local development with SSH
|
||||||
git clone "git@github.com:selfxyz/${repo_name}.git" "$dir_name" || {
|
git clone "git@github.com:selfxyz/${repo_name}.git" "$dir_name" || {
|
||||||
log "ERROR: Failed to clone $repo_name with SSH"
|
log "ERROR: Failed to clone $repo_name with SSH"
|
||||||
log "Please ensure you have SSH access to the repository or set SELFXYZ_INTERNAL_REPO_PAT"
|
log "Please ensure you have SSH access to the repository or set SELFXYZ_APP_TOKEN/SELFXYZ_INTERNAL_REPO_PAT"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
log "ERROR: No authentication method available for cloning $repo_name"
|
log "ERROR: No authentication method available for cloning $repo_name"
|
||||||
log "Please either:"
|
log "Please either:"
|
||||||
log " - Set up SSH access (for local development)"
|
log " - Set up SSH access (for local development)"
|
||||||
log " - Set SELFXYZ_INTERNAL_REPO_PAT environment variable (for CI)"
|
log " - Set SELFXYZ_APP_TOKEN or SELFXYZ_INTERNAL_REPO_PAT environment variable (for CI)"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -194,14 +200,15 @@ log "✅ Package files backed up successfully"
|
|||||||
# Install SDK from tarball in app with timeout
|
# Install SDK from tarball in app with timeout
|
||||||
log "Installing SDK as real files..."
|
log "Installing SDK as real files..."
|
||||||
if is_ci; then
|
if is_ci; then
|
||||||
# Temporarily unset PAT to skip private modules during SDK installation
|
# Temporarily unset both auth tokens to skip private modules during SDK installation
|
||||||
env -u SELFXYZ_INTERNAL_REPO_PAT timeout 180 yarn add "@selfxyz/mobile-sdk-alpha@file:$TARBALL_PATH" || {
|
# Both tokens must be unset to prevent setup-private-modules.cjs from attempting clones
|
||||||
|
env -u SELFXYZ_INTERNAL_REPO_PAT -u SELFXYZ_APP_TOKEN timeout 180 yarn add "@selfxyz/mobile-sdk-alpha@file:$TARBALL_PATH" || {
|
||||||
log "SDK installation timed out after 3 minutes"
|
log "SDK installation timed out after 3 minutes"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
# Temporarily unset PAT to skip private modules during SDK installation
|
# Temporarily unset both auth tokens to skip private modules during SDK installation
|
||||||
env -u SELFXYZ_INTERNAL_REPO_PAT yarn add "@selfxyz/mobile-sdk-alpha@file:$TARBALL_PATH"
|
env -u SELFXYZ_INTERNAL_REPO_PAT -u SELFXYZ_APP_TOKEN yarn add "@selfxyz/mobile-sdk-alpha@file:$TARBALL_PATH"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Verify installation (check for AAR file in both local and hoisted locations)
|
# Verify installation (check for AAR file in both local and hoisted locations)
|
||||||
|
|||||||
@@ -29,8 +29,9 @@ const PRIVATE_MODULES = [
|
|||||||
|
|
||||||
// Environment detection
|
// Environment detection
|
||||||
// CI is set by GitHub Actions, CircleCI, etc. Check for truthy value
|
// CI is set by GitHub Actions, CircleCI, etc. Check for truthy value
|
||||||
const isCI = !!process.env.CI || process.env.GITHUB_ACTIONS === 'true';
|
const isCI = process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true';
|
||||||
const repoToken = process.env.SELFXYZ_INTERNAL_REPO_PAT;
|
const repoToken = process.env.SELFXYZ_INTERNAL_REPO_PAT;
|
||||||
|
const appToken = process.env.SELFXYZ_APP_TOKEN; // GitHub App installation token
|
||||||
const isDryRun = process.env.DRY_RUN === 'true';
|
const isDryRun = process.env.DRY_RUN === 'true';
|
||||||
|
|
||||||
// Platform detection for Android-specific modules
|
// Platform detection for Android-specific modules
|
||||||
@@ -150,13 +151,17 @@ function clonePrivateRepo(repoName, localPath) {
|
|||||||
|
|
||||||
let cloneUrl;
|
let cloneUrl;
|
||||||
|
|
||||||
if (isCI && repoToken) {
|
if (isCI && appToken) {
|
||||||
|
// CI environment with GitHub App installation token
|
||||||
|
log('CI detected: Using SELFXYZ_APP_TOKEN for clone', 'info');
|
||||||
|
cloneUrl = `https://x-access-token:${appToken}@github.com/${GITHUB_ORG}/${repoName}.git`;
|
||||||
|
} else if (isCI && repoToken) {
|
||||||
// CI environment with Personal Access Token
|
// CI environment with Personal Access Token
|
||||||
log('CI detected: Using SELFXYZ_INTERNAL_REPO_PAT for clone', 'info');
|
log('CI detected: Using SELFXYZ_INTERNAL_REPO_PAT for clone', 'info');
|
||||||
cloneUrl = `https://${repoToken}@github.com/${GITHUB_ORG}/${repoName}.git`;
|
cloneUrl = `https://${repoToken}@github.com/${GITHUB_ORG}/${repoName}.git`;
|
||||||
} else if (isCI) {
|
} else if (isCI) {
|
||||||
log(
|
log(
|
||||||
'CI environment detected but SELFXYZ_INTERNAL_REPO_PAT not available - skipping private module setup',
|
'CI environment detected but no token available - skipping private module setup',
|
||||||
'info',
|
'info',
|
||||||
);
|
);
|
||||||
log(
|
log(
|
||||||
@@ -173,7 +178,7 @@ function clonePrivateRepo(repoName, localPath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Security: Use quiet mode for credentialed URLs to prevent token exposure
|
// Security: Use quiet mode for credentialed URLs to prevent token exposure
|
||||||
const isCredentialedUrl = isCI && repoToken;
|
const isCredentialedUrl = isCI && (appToken || repoToken);
|
||||||
const quietFlag = isCredentialedUrl ? '--quiet' : '';
|
const quietFlag = isCredentialedUrl ? '--quiet' : '';
|
||||||
const targetDir = path.basename(localPath);
|
const targetDir = path.basename(localPath);
|
||||||
const cloneCommand = `git clone --branch ${BRANCH} --single-branch --depth 1 ${quietFlag} "${cloneUrl}" "${targetDir}"`;
|
const cloneCommand = `git clone --branch ${BRANCH} --single-branch --depth 1 ${quietFlag} "${cloneUrl}" "${targetDir}"`;
|
||||||
@@ -190,7 +195,7 @@ function clonePrivateRepo(repoName, localPath) {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isCI) {
|
if (isCI) {
|
||||||
log(
|
log(
|
||||||
'Clone failed in CI environment. Check SELFXYZ_INTERNAL_REPO_PAT permissions.',
|
'Clone failed in CI environment. Check SELFXYZ_APP_TOKEN or SELFXYZ_INTERNAL_REPO_PAT permissions.',
|
||||||
'error',
|
'error',
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -231,7 +236,7 @@ function setupPrivateModule(module) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Security: Remove credential-embedded remote URL after clone
|
// Security: Remove credential-embedded remote URL after clone
|
||||||
if (isCI && repoToken && !isDryRun) {
|
if (isCI && (appToken || repoToken) && !isDryRun) {
|
||||||
scrubGitRemoteUrl(localPath, repoName);
|
scrubGitRemoteUrl(localPath, repoName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,6 +280,11 @@ function setupAndroidPassportReader() {
|
|||||||
`Setup complete: ${successCount}/${PRIVATE_MODULES.length} modules cloned`,
|
`Setup complete: ${successCount}/${PRIVATE_MODULES.length} modules cloned`,
|
||||||
'warning',
|
'warning',
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
log(
|
||||||
|
'No private modules were cloned - this is expected for forked PRs',
|
||||||
|
'info',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,7 @@ import React from 'react';
|
|||||||
import { ArrowLeft, ArrowRight, RotateCcw } from '@tamagui/lucide-icons';
|
import { ArrowLeft, ArrowRight, RotateCcw } from '@tamagui/lucide-icons';
|
||||||
|
|
||||||
import { Button, XStack, YStack } from '@selfxyz/mobile-sdk-alpha/components';
|
import { Button, XStack, YStack } from '@selfxyz/mobile-sdk-alpha/components';
|
||||||
import {
|
import { black, slate400 } from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||||
black,
|
|
||||||
slate50,
|
|
||||||
slate400,
|
|
||||||
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
|
||||||
|
|
||||||
import { buttonTap } from '@/integrations/haptics';
|
import { buttonTap } from '@/integrations/haptics';
|
||||||
|
|
||||||
@@ -23,8 +19,7 @@ export interface WebViewFooterProps {
|
|||||||
onOpenInBrowser: () => void;
|
onOpenInBrowser: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const iconSize = 22;
|
const iconSize = 24;
|
||||||
const buttonSize = 36;
|
|
||||||
|
|
||||||
export const WebViewFooter: React.FC<WebViewFooterProps> = ({
|
export const WebViewFooter: React.FC<WebViewFooterProps> = ({
|
||||||
canGoBack,
|
canGoBack,
|
||||||
@@ -42,19 +37,13 @@ export const WebViewFooter: React.FC<WebViewFooterProps> = ({
|
|||||||
) => (
|
) => (
|
||||||
<Button
|
<Button
|
||||||
key={key}
|
key={key}
|
||||||
size="$4"
|
|
||||||
unstyled
|
unstyled
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
hitSlop={{ top: 20, bottom: 20, left: 20, right: 20 }}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
buttonTap();
|
buttonTap();
|
||||||
onPress();
|
onPress();
|
||||||
}}
|
}}
|
||||||
backgroundColor={slate50}
|
|
||||||
borderRadius={buttonSize / 2}
|
|
||||||
width={buttonSize}
|
|
||||||
height={buttonSize}
|
|
||||||
alignItems="center"
|
|
||||||
justifyContent="center"
|
|
||||||
opacity={disabled ? 0.5 : 1}
|
opacity={disabled ? 0.5 : 1}
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
@@ -62,7 +51,7 @@ export const WebViewFooter: React.FC<WebViewFooterProps> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<YStack gap={12} paddingVertical={12} width="100%">
|
<YStack gap={4} paddingVertical={4} paddingHorizontal={5} width="100%">
|
||||||
<XStack justifyContent="space-between" alignItems="center" width="100%">
|
<XStack justifyContent="space-between" alignItems="center" width="100%">
|
||||||
{renderIconButton(
|
{renderIconButton(
|
||||||
'back',
|
'back',
|
||||||
|
|||||||
17
app/src/components/navbar/HeadlessNavForEuclid.tsx
Normal file
17
app/src/components/navbar/HeadlessNavForEuclid.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||||
|
// SPDX-License-Identifier: BUSL-1.1
|
||||||
|
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||||
|
|
||||||
|
import { SystemBars } from 'react-native-edge-to-edge';
|
||||||
|
import type { NativeStackHeaderProps } from '@react-navigation/native-stack';
|
||||||
|
|
||||||
|
export const HeadlessNavForEuclid = (props: NativeStackHeaderProps) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SystemBars
|
||||||
|
style={props.options.statusBarStyle}
|
||||||
|
hidden={props.options.statusBarHidden}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -30,9 +30,9 @@ export const WebViewNavBar: React.FC<WebViewNavBarProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<XStack
|
<XStack
|
||||||
paddingHorizontal={20}
|
|
||||||
paddingVertical={10}
|
paddingVertical={10}
|
||||||
paddingTop={insets.top + 10}
|
paddingTop={insets.top + 10}
|
||||||
|
paddingHorizontal={16}
|
||||||
gap={14}
|
gap={14}
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
backgroundColor="white"
|
backgroundColor="white"
|
||||||
@@ -50,7 +50,12 @@ export const WebViewNavBar: React.FC<WebViewNavBarProps> = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Center: Title */}
|
{/* Center: Title */}
|
||||||
<XStack flex={1} alignItems="center" justifyContent="center">
|
<XStack
|
||||||
|
flex={1}
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
paddingHorizontal={8}
|
||||||
|
>
|
||||||
<Text style={styles.title} numberOfLines={1}>
|
<Text style={styles.title} numberOfLines={1}>
|
||||||
{title?.toUpperCase() || 'PAGE TITLE'}
|
{title?.toUpperCase() || 'PAGE TITLE'}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
16
app/src/hooks/useSafeAreaInsets.ts
Normal file
16
app/src/hooks/useSafeAreaInsets.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||||
|
// SPDX-License-Identifier: BUSL-1.1
|
||||||
|
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||||
|
|
||||||
|
import { Platform } from 'react-native';
|
||||||
|
import { useSafeAreaInsets as useSafeAreaInsetsOriginal } from 'react-native-safe-area-context';
|
||||||
|
|
||||||
|
// gives bare minimums in case safe area doesnt provide for example space for status bar icons.
|
||||||
|
export function useSafeAreaInsets() {
|
||||||
|
const insets = useSafeAreaInsetsOriginal();
|
||||||
|
const minimum = Platform.select({ ios: 54, android: 26, web: 48 });
|
||||||
|
return {
|
||||||
|
...insets,
|
||||||
|
top: Math.max(insets.top, minimum || 0),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
white,
|
white,
|
||||||
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||||
|
|
||||||
|
import { HeadlessNavForEuclid } from '@/components/navbar/HeadlessNavForEuclid';
|
||||||
import AccountRecoveryChoiceScreen from '@/screens/account/recovery/AccountRecoveryChoiceScreen';
|
import AccountRecoveryChoiceScreen from '@/screens/account/recovery/AccountRecoveryChoiceScreen';
|
||||||
import AccountRecoveryScreen from '@/screens/account/recovery/AccountRecoveryScreen';
|
import AccountRecoveryScreen from '@/screens/account/recovery/AccountRecoveryScreen';
|
||||||
import DocumentDataNotFoundScreen from '@/screens/account/recovery/DocumentDataNotFoundScreen';
|
import DocumentDataNotFoundScreen from '@/screens/account/recovery/DocumentDataNotFoundScreen';
|
||||||
@@ -17,6 +18,7 @@ import RecoverWithPhraseScreen from '@/screens/account/recovery/RecoverWithPhras
|
|||||||
import CloudBackupScreen from '@/screens/account/settings/CloudBackupScreen';
|
import CloudBackupScreen from '@/screens/account/settings/CloudBackupScreen';
|
||||||
import SettingsScreen from '@/screens/account/settings/SettingsScreen';
|
import SettingsScreen from '@/screens/account/settings/SettingsScreen';
|
||||||
import ShowRecoveryPhraseScreen from '@/screens/account/settings/ShowRecoveryPhraseScreen';
|
import ShowRecoveryPhraseScreen from '@/screens/account/settings/ShowRecoveryPhraseScreen';
|
||||||
|
import { IS_EUCLID_ENABLED } from '@/utils/devUtils';
|
||||||
|
|
||||||
const accountScreens = {
|
const accountScreens = {
|
||||||
AccountRecovery: {
|
AccountRecovery: {
|
||||||
@@ -79,14 +81,22 @@ const accountScreens = {
|
|||||||
screens: {},
|
screens: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
ShowRecoveryPhrase: {
|
ShowRecoveryPhrase: {
|
||||||
screen: ShowRecoveryPhraseScreen,
|
screen: ShowRecoveryPhraseScreen,
|
||||||
options: {
|
options: IS_EUCLID_ENABLED
|
||||||
title: 'Recovery Phrase',
|
? ({
|
||||||
headerStyle: {
|
headerShown: true,
|
||||||
backgroundColor: white,
|
header: HeadlessNavForEuclid,
|
||||||
},
|
statusBarStyle: ShowRecoveryPhraseScreen.statusBarStyle,
|
||||||
} as NativeStackNavigationOptions,
|
statusBarHidden: ShowRecoveryPhraseScreen.statusBarHidden,
|
||||||
|
} as NativeStackNavigationOptions)
|
||||||
|
: ({
|
||||||
|
title: 'Recovery Phrase',
|
||||||
|
headerStyle: {
|
||||||
|
backgroundColor: white,
|
||||||
|
},
|
||||||
|
} as NativeStackNavigationOptions),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type { NativeStackNavigationOptions } from '@react-navigation/native-stac
|
|||||||
import { black, white } from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
import { black, white } from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||||
|
|
||||||
import { AadhaarNavBar, IdDetailsNavBar } from '@/components/navbar';
|
import { AadhaarNavBar, IdDetailsNavBar } from '@/components/navbar';
|
||||||
|
import { HeadlessNavForEuclid } from '@/components/navbar/HeadlessNavForEuclid';
|
||||||
import AadhaarUploadedSuccessScreen from '@/screens/documents/aadhaar/AadhaarUploadedSuccessScreen';
|
import AadhaarUploadedSuccessScreen from '@/screens/documents/aadhaar/AadhaarUploadedSuccessScreen';
|
||||||
import AadhaarUploadErrorScreen from '@/screens/documents/aadhaar/AadhaarUploadErrorScreen';
|
import AadhaarUploadErrorScreen from '@/screens/documents/aadhaar/AadhaarUploadErrorScreen';
|
||||||
import AadhaarUploadScreen from '@/screens/documents/aadhaar/AadhaarUploadScreen';
|
import AadhaarUploadScreen from '@/screens/documents/aadhaar/AadhaarUploadScreen';
|
||||||
@@ -76,7 +77,10 @@ const documentsScreens = {
|
|||||||
CountryPicker: {
|
CountryPicker: {
|
||||||
screen: CountryPickerScreen,
|
screen: CountryPickerScreen,
|
||||||
options: {
|
options: {
|
||||||
headerShown: false,
|
header: HeadlessNavForEuclid,
|
||||||
|
statusBarHidden: CountryPickerScreen.statusBar?.hidden,
|
||||||
|
statusBarStyle: CountryPickerScreen.statusBar?.style,
|
||||||
|
headerShown: true,
|
||||||
} as NativeStackNavigationOptions,
|
} as NativeStackNavigationOptions,
|
||||||
},
|
},
|
||||||
IDPicker: {
|
IDPicker: {
|
||||||
|
|||||||
@@ -2,21 +2,85 @@
|
|||||||
// SPDX-License-Identifier: BUSL-1.1
|
// SPDX-License-Identifier: BUSL-1.1
|
||||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||||
|
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback, useEffect, useRef } from 'react';
|
||||||
|
import Clipboard from '@react-native-clipboard/clipboard';
|
||||||
|
|
||||||
|
import type { RecoveryPhraseVariant } from '@selfxyz/euclid';
|
||||||
|
import { RecoveryPhraseScreen } from '@selfxyz/euclid';
|
||||||
|
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
|
||||||
import { Description } from '@selfxyz/mobile-sdk-alpha/components';
|
import { Description } from '@selfxyz/mobile-sdk-alpha/components';
|
||||||
|
|
||||||
import Mnemonic from '@/components/Mnemonic';
|
import Mnemonic from '@/components/Mnemonic';
|
||||||
import useMnemonic from '@/hooks/useMnemonic';
|
import useMnemonic from '@/hooks/useMnemonic';
|
||||||
|
import { useSafeAreaInsets } from '@/hooks/useSafeAreaInsets';
|
||||||
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
|
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
|
||||||
|
import { useSettingStore } from '@/stores/settingStore';
|
||||||
|
import { IS_EUCLID_ENABLED } from '@/utils/devUtils';
|
||||||
|
|
||||||
const ShowRecoveryPhraseScreen: React.FC = () => {
|
function useCopyRecoveryPhrase(mnemonic: string[] | undefined) {
|
||||||
|
const [copied, setCopied] = React.useState(false);
|
||||||
|
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
|
const onCopy = useCallback(() => {
|
||||||
|
if (!mnemonic) return;
|
||||||
|
Clipboard.setString(mnemonic.join(' '));
|
||||||
|
setCopied(true);
|
||||||
|
|
||||||
|
// Clear any existing timeout
|
||||||
|
if (timeoutRef.current) {
|
||||||
|
clearTimeout(timeoutRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set new timeout and store its ID
|
||||||
|
timeoutRef.current = setTimeout(() => setCopied(false), 2500);
|
||||||
|
}, [mnemonic]);
|
||||||
|
|
||||||
|
// Cleanup timeout on unmount
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (timeoutRef.current) {
|
||||||
|
clearTimeout(timeoutRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { copied, onCopy };
|
||||||
|
}
|
||||||
|
|
||||||
|
const ShowRecoveryPhraseScreen: React.FC & {
|
||||||
|
statusBarStyle: string;
|
||||||
|
statusBarHidden: boolean;
|
||||||
|
} = () => {
|
||||||
const { mnemonic, loadMnemonic } = useMnemonic();
|
const { mnemonic, loadMnemonic } = useMnemonic();
|
||||||
|
const self = useSelfClient();
|
||||||
|
const { copied, onCopy } = useCopyRecoveryPhrase(mnemonic);
|
||||||
|
const { setHasViewedRecoveryPhrase } = useSettingStore();
|
||||||
|
|
||||||
const onRevealWords = useCallback(async () => {
|
const onReveal = useCallback(async () => {
|
||||||
await loadMnemonic();
|
await loadMnemonic();
|
||||||
}, [loadMnemonic]);
|
setHasViewedRecoveryPhrase(true);
|
||||||
|
}, [loadMnemonic, setHasViewedRecoveryPhrase]);
|
||||||
|
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
|
if (IS_EUCLID_ENABLED) {
|
||||||
|
const variant: RecoveryPhraseVariant = !mnemonic
|
||||||
|
? 'hidden'
|
||||||
|
: copied
|
||||||
|
? 'copied'
|
||||||
|
: 'revealed';
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<RecoveryPhraseScreen
|
||||||
|
insets={insets}
|
||||||
|
onReveal={onReveal}
|
||||||
|
words={mnemonic}
|
||||||
|
onBack={self.goBack}
|
||||||
|
variant={variant}
|
||||||
|
onCopy={onCopy}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<ExpandableBottomLayout.Layout backgroundColor="white">
|
<ExpandableBottomLayout.Layout backgroundColor="white">
|
||||||
<ExpandableBottomLayout.BottomSection
|
<ExpandableBottomLayout.BottomSection
|
||||||
@@ -24,7 +88,7 @@ const ShowRecoveryPhraseScreen: React.FC = () => {
|
|||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
gap={20}
|
gap={20}
|
||||||
>
|
>
|
||||||
<Mnemonic words={mnemonic} onRevealWords={onRevealWords} />
|
<Mnemonic words={mnemonic} onRevealWords={loadMnemonic} />
|
||||||
<Description>
|
<Description>
|
||||||
This phrase is the only way to recover your account. Keep it secret,
|
This phrase is the only way to recover your account. Keep it secret,
|
||||||
keep it safe.
|
keep it safe.
|
||||||
@@ -35,3 +99,7 @@ const ShowRecoveryPhraseScreen: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default ShowRecoveryPhraseScreen;
|
export default ShowRecoveryPhraseScreen;
|
||||||
|
|
||||||
|
ShowRecoveryPhraseScreen.statusBarHidden =
|
||||||
|
RecoveryPhraseScreen.statusBar.hidden;
|
||||||
|
ShowRecoveryPhraseScreen.statusBarStyle = RecoveryPhraseScreen.statusBar.style;
|
||||||
|
|||||||
@@ -2,8 +2,21 @@
|
|||||||
// SPDX-License-Identifier: BUSL-1.1
|
// SPDX-License-Identifier: BUSL-1.1
|
||||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||||
|
|
||||||
|
import type React from 'react';
|
||||||
|
|
||||||
import SDKCountryPickerScreen from '@selfxyz/mobile-sdk-alpha/onboarding/country-picker-screen';
|
import SDKCountryPickerScreen from '@selfxyz/mobile-sdk-alpha/onboarding/country-picker-screen';
|
||||||
|
|
||||||
export default function CountryPickerScreen() {
|
import { useSafeAreaInsets } from '@/hooks/useSafeAreaInsets';
|
||||||
return <SDKCountryPickerScreen />;
|
|
||||||
}
|
type CountryPickerScreenComponent = React.FC & {
|
||||||
|
statusBar: typeof SDKCountryPickerScreen.statusBar;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CountryPickerScreen: CountryPickerScreenComponent = () => {
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
|
return <SDKCountryPickerScreen insets={insets} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
CountryPickerScreen.statusBar = SDKCountryPickerScreen.statusBar;
|
||||||
|
|
||||||
|
export default CountryPickerScreen;
|
||||||
|
|||||||
@@ -5,8 +5,10 @@
|
|||||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
|
Alert,
|
||||||
BackHandler,
|
BackHandler,
|
||||||
Linking,
|
Linking,
|
||||||
|
Platform,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
View,
|
View,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
@@ -26,6 +28,14 @@ import { WebViewFooter } from '@/components/WebViewFooter';
|
|||||||
import { selfUrl } from '@/consts/links';
|
import { selfUrl } from '@/consts/links';
|
||||||
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
|
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
|
||||||
import type { SharedRoutesParamList } from '@/navigation/types';
|
import type { SharedRoutesParamList } from '@/navigation/types';
|
||||||
|
import {
|
||||||
|
DISALLOWED_SCHEMES,
|
||||||
|
isAllowedAboutUrl,
|
||||||
|
isHostnameMatch,
|
||||||
|
isTrustedDomain,
|
||||||
|
isUserInitiatedTopFrameNavigation,
|
||||||
|
shouldAlwaysOpenExternally,
|
||||||
|
} from '@/utils/webview';
|
||||||
|
|
||||||
export interface WebViewScreenParams {
|
export interface WebViewScreenParams {
|
||||||
url: string;
|
url: string;
|
||||||
@@ -41,6 +51,25 @@ type WebViewScreenProps = NativeStackScreenProps<
|
|||||||
>;
|
>;
|
||||||
|
|
||||||
const defaultUrl = selfUrl;
|
const defaultUrl = selfUrl;
|
||||||
|
const fallbackUrl = 'https://apps.self.xyz';
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
webViewContainer: {
|
||||||
|
flex: 1,
|
||||||
|
alignSelf: 'stretch',
|
||||||
|
backgroundColor: white,
|
||||||
|
},
|
||||||
|
webView: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: white,
|
||||||
|
},
|
||||||
|
loadingOverlay: {
|
||||||
|
...StyleSheet.absoluteFillObject,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundColor: 'rgba(255,255,255,0.5)',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const WebViewScreen: React.FC<WebViewScreenProps> = ({ route }) => {
|
export const WebViewScreen: React.FC<WebViewScreenProps> = ({ route }) => {
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
@@ -50,24 +79,83 @@ export const WebViewScreen: React.FC<WebViewScreenProps> = ({ route }) => {
|
|||||||
const isHttpUrl = useCallback((value?: string) => {
|
const isHttpUrl = useCallback((value?: string) => {
|
||||||
return typeof value === 'string' && /^https?:\/\//i.test(value);
|
return typeof value === 'string' && /^https?:\/\//i.test(value);
|
||||||
}, []);
|
}, []);
|
||||||
const initialUrl = useMemo(
|
const initialUrl = useMemo(() => {
|
||||||
() => (isHttpUrl(url) ? url : defaultUrl),
|
if (isHttpUrl(url) && isTrustedDomain(url)) {
|
||||||
[isHttpUrl, url],
|
return url;
|
||||||
);
|
}
|
||||||
|
if (isHttpUrl(defaultUrl) && isTrustedDomain(defaultUrl)) {
|
||||||
|
return defaultUrl;
|
||||||
|
}
|
||||||
|
return fallbackUrl;
|
||||||
|
}, [isHttpUrl, url]);
|
||||||
const webViewRef = useRef<WebViewType>(null);
|
const webViewRef = useRef<WebViewType>(null);
|
||||||
const [canGoBackInWebView, setCanGoBackInWebView] = useState(false);
|
const [canGoBackInWebView, setCanGoBackInWebView] = useState(false);
|
||||||
const [canGoForwardInWebView, setCanGoForwardInWebView] = useState(false);
|
const [canGoForwardInWebView, setCanGoForwardInWebView] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [currentUrl, setCurrentUrl] = useState(initialUrl);
|
const [currentUrl, setCurrentUrl] = useState(initialUrl);
|
||||||
const [pageTitle, setPageTitle] = useState<string | undefined>(title);
|
const [pageTitle, setPageTitle] = useState<string | undefined>(title);
|
||||||
|
const [isSessionTrusted, setIsSessionTrusted] = useState(
|
||||||
|
isTrustedDomain(initialUrl),
|
||||||
|
);
|
||||||
|
|
||||||
const derivedTitle = pageTitle || title || currentUrl;
|
const derivedTitle = pageTitle || title || currentUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a confirmation dialog before opening a URL externally.
|
||||||
|
* Returns true if user confirms, false if they cancel.
|
||||||
|
*/
|
||||||
|
const confirmExternalNavigation = useCallback(
|
||||||
|
(context: 'wallet' | 'deep-link' | 'external-site'): Promise<boolean> => {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const messages: Record<
|
||||||
|
typeof context,
|
||||||
|
{ title: string; body: string }
|
||||||
|
> = {
|
||||||
|
wallet: {
|
||||||
|
title: 'Open in Browser',
|
||||||
|
body: 'This will open in your browser to complete the wallet connection.',
|
||||||
|
},
|
||||||
|
'deep-link': {
|
||||||
|
title: 'Open External App',
|
||||||
|
body: 'This will open an external app.',
|
||||||
|
},
|
||||||
|
'external-site': {
|
||||||
|
title: 'Open in Browser',
|
||||||
|
body: 'This will open an external website in your browser.',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const { title: alertTitle, body } = messages[context];
|
||||||
|
|
||||||
|
Alert.alert(alertTitle, body, [
|
||||||
|
{ text: 'Cancel', style: 'cancel', onPress: () => resolve(false) },
|
||||||
|
{ text: 'Open', onPress: () => resolve(true) },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const openUrl = useCallback(async (targetUrl: string) => {
|
const openUrl = useCallback(async (targetUrl: string) => {
|
||||||
// Allow only safe external schemes
|
// Block disallowed schemes (blacklist approach)
|
||||||
if (!/^(https?|mailto|tel):/i.test(targetUrl)) {
|
// Allow everything else - more practical than maintaining a whitelist
|
||||||
|
const isDisallowed = DISALLOWED_SCHEMES.some(scheme =>
|
||||||
|
targetUrl.toLowerCase().startsWith(scheme.toLowerCase()),
|
||||||
|
);
|
||||||
|
if (isDisallowed) {
|
||||||
|
// Block disallowed schemes - don't attempt to open
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Block about:blank and similar about: URLs - they're not meant to be opened externally
|
||||||
|
if (targetUrl.toLowerCase().startsWith('about:')) {
|
||||||
|
// Silently ignore about: URLs - they're internal browser navigation
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Validate URL has a valid scheme pattern
|
||||||
|
if (!/^[a-zA-Z][a-zA-Z0-9+.-]*:/i.test(targetUrl)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Attempt to open the URL
|
||||||
try {
|
try {
|
||||||
const supported = await Linking.canOpenURL(targetUrl);
|
const supported = await Linking.canOpenURL(targetUrl);
|
||||||
if (supported) {
|
if (supported) {
|
||||||
@@ -115,16 +203,23 @@ export const WebViewScreen: React.FC<WebViewScreenProps> = ({ route }) => {
|
|||||||
const subscription = BackHandler.addEventListener(
|
const subscription = BackHandler.addEventListener(
|
||||||
'hardwareBackPress',
|
'hardwareBackPress',
|
||||||
() => {
|
() => {
|
||||||
|
// First try to go back in WebView if possible
|
||||||
if (canGoBackInWebView) {
|
if (canGoBackInWebView) {
|
||||||
webViewRef.current?.goBack();
|
webViewRef.current?.goBack();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
// If WebView can't go back, close the WebView screen (go back in navigation)
|
||||||
|
if (navigation?.canGoBack()) {
|
||||||
|
navigation.goBack();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Only allow default behavior (close app) if navigation can't go back
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return () => subscription.remove();
|
return () => subscription.remove();
|
||||||
}, [canGoBackInWebView]),
|
}, [canGoBackInWebView, navigation]),
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -134,6 +229,7 @@ export const WebViewScreen: React.FC<WebViewScreenProps> = ({ route }) => {
|
|||||||
alignItems="stretch"
|
alignItems="stretch"
|
||||||
justifyContent="flex-start"
|
justifyContent="flex-start"
|
||||||
padding={0}
|
padding={0}
|
||||||
|
paddingHorizontal={5}
|
||||||
>
|
>
|
||||||
<WebViewNavBar
|
<WebViewNavBar
|
||||||
title={derivedTitle}
|
title={derivedTitle}
|
||||||
@@ -149,18 +245,158 @@ export const WebViewScreen: React.FC<WebViewScreenProps> = ({ route }) => {
|
|||||||
<WebView
|
<WebView
|
||||||
ref={webViewRef}
|
ref={webViewRef}
|
||||||
onShouldStartLoadWithRequest={req => {
|
onShouldStartLoadWithRequest={req => {
|
||||||
// Open non-http(s) externally, block in WebView
|
const isHttps = /^https:\/\//i.test(req.url);
|
||||||
if (!/^https?:\/\//i.test(req.url)) {
|
|
||||||
openUrl(req.url);
|
// Allow about:blank/srcdoc during trusted sessions (some wallets use this before redirecting)
|
||||||
|
if (isSessionTrusted && isAllowedAboutUrl(req.url)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// iOS-specific: Detect WalletConnect attestation from Aave and kick to Safari
|
||||||
|
// WalletConnect doesn't work properly in WKWebView for Coinbase Wallet connections
|
||||||
|
// Use hostname matching to prevent spoofing (e.g., evil.com/?next=verify.walletconnect.org)
|
||||||
|
if (
|
||||||
|
Platform.OS === 'ios' &&
|
||||||
|
isHostnameMatch(req.url, 'verify.walletconnect.org') &&
|
||||||
|
req.mainDocumentURL &&
|
||||||
|
isHostnameMatch(req.mainDocumentURL, 'app.aave.com')
|
||||||
|
) {
|
||||||
|
// Kick parent page to Safari for wallet connection
|
||||||
|
confirmExternalNavigation('wallet').then(confirmed => {
|
||||||
|
if (confirmed) {
|
||||||
|
openUrl(currentUrl);
|
||||||
|
}
|
||||||
|
});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
|
// Open non-http(s) schemes externally (mailto, tel, etc.)
|
||||||
|
// iOS: only allow top-frame, user-initiated navigations to prevent
|
||||||
|
// drive-by deep-linking via iframes on trusted partner sites
|
||||||
|
if (!/^https?:\/\//i.test(req.url)) {
|
||||||
|
if (isUserInitiatedTopFrameNavigation(req)) {
|
||||||
|
// Show confirmation before opening deep-link schemes
|
||||||
|
confirmExternalNavigation('deep-link').then(confirmed => {
|
||||||
|
if (confirmed) {
|
||||||
|
openUrl(req.url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enforce "always open externally" policy before any other checks
|
||||||
|
// (e.g., keys.coinbase.com requires window.opener in full browser)
|
||||||
|
if (shouldAlwaysOpenExternally(req.url)) {
|
||||||
|
// Show confirmation before redirecting to external wallet
|
||||||
|
confirmExternalNavigation('wallet').then(confirmed => {
|
||||||
|
if (confirmed) {
|
||||||
|
// Open the current page externally to maintain window.opener
|
||||||
|
openUrl(currentUrl);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const trusted = isTrustedDomain(req.url);
|
||||||
|
|
||||||
|
// Allow trusted entrypoints and mark session trusted
|
||||||
|
if (trusted) {
|
||||||
|
if (!isSessionTrusted) {
|
||||||
|
setIsSessionTrusted(true);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parent-trusted session model: allow HTTPS child navigations
|
||||||
|
// after a trusted entrypoint to avoid breaking on partner deps.
|
||||||
|
if (isSessionTrusted && isHttps) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Untrusted navigation without a trusted session: open externally
|
||||||
|
// iOS: only allow top-frame, user-initiated navigations
|
||||||
|
if (isUserInitiatedTopFrameNavigation(req)) {
|
||||||
|
// Show confirmation before opening untrusted external site
|
||||||
|
confirmExternalNavigation('external-site').then(confirmed => {
|
||||||
|
if (confirmed) {
|
||||||
|
openUrl(req.url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}}
|
}}
|
||||||
|
onOpenWindow={syntheticEvent => {
|
||||||
|
// Handle links that try to open in new window (target="_blank")
|
||||||
|
const { nativeEvent } = syntheticEvent;
|
||||||
|
const targetUrl = nativeEvent.targetUrl;
|
||||||
|
|
||||||
|
if (targetUrl) {
|
||||||
|
// Coinbase wallet uses window.opener.postMessage from the popup back to
|
||||||
|
// the parent page. If we only open the popup externally and keep the
|
||||||
|
// parent inside the WebView, the popup cannot find window.opener and the
|
||||||
|
// SDK times out. Redirect the parent page (currentUrl) to a real browser
|
||||||
|
// context; if we somehow don't know the parent URL, fall back to opening
|
||||||
|
// the popup target directly.
|
||||||
|
if (shouldAlwaysOpenExternally(targetUrl)) {
|
||||||
|
// Show confirmation before redirecting to external wallet
|
||||||
|
confirmExternalNavigation('wallet').then(confirmed => {
|
||||||
|
if (confirmed) {
|
||||||
|
openUrl(currentUrl || targetUrl);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some sites open about:blank/srcdoc before redirecting; allow silently
|
||||||
|
if (isSessionTrusted && isAllowedAboutUrl(targetUrl)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow trusted domains to load in the current WebView
|
||||||
|
const trusted = isTrustedDomain(targetUrl);
|
||||||
|
if (trusted) {
|
||||||
|
if (!isSessionTrusted) {
|
||||||
|
setIsSessionTrusted(true);
|
||||||
|
}
|
||||||
|
webViewRef.current?.injectJavaScript(
|
||||||
|
`window.location.href = ${JSON.stringify(targetUrl)};`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parent-trusted session model: allow HTTPS child navigations via window.open
|
||||||
|
// after a trusted entrypoint to avoid breaking on partner deps.
|
||||||
|
if (isSessionTrusted && /^https:\/\//i.test(targetUrl)) {
|
||||||
|
webViewRef.current?.injectJavaScript(
|
||||||
|
`window.location.href = ${JSON.stringify(targetUrl)};`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Security: Block non-HTTPS/non-trusted window.open calls to prevent
|
||||||
|
// drive-by deep-linking from iframes on trusted sites. Unlike
|
||||||
|
// onShouldStartLoadWithRequest, onOpenWindow doesn't expose frame-origin
|
||||||
|
// metadata, so we cannot verify if this is user-initiated top-frame
|
||||||
|
// navigation. Block silently to maintain security without breaking UX.
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
// Enable multiple windows to let WKWebView forward window.open;
|
||||||
|
// we still force navigation into the same WebView via onOpenWindow.
|
||||||
|
setSupportMultipleWindows
|
||||||
source={{ uri: initialUrl }}
|
source={{ uri: initialUrl }}
|
||||||
onNavigationStateChange={(event: WebViewNavigation) => {
|
onNavigationStateChange={(event: WebViewNavigation) => {
|
||||||
setCanGoBackInWebView(event.canGoBack);
|
setCanGoBackInWebView(event.canGoBack);
|
||||||
setCanGoForwardInWebView(event.canGoForward);
|
setCanGoForwardInWebView(event.canGoForward);
|
||||||
setCurrentUrl(prev => (isHttpUrl(event.url) ? event.url : prev));
|
setCurrentUrl(prev => (isHttpUrl(event.url) ? event.url : prev));
|
||||||
|
// Only mark session as trusted if the domain is trusted AND not in always-external list
|
||||||
|
// (e.g., keys.coinbase.com should never establish a trusted session)
|
||||||
|
if (
|
||||||
|
isTrustedDomain(event.url) &&
|
||||||
|
!shouldAlwaysOpenExternally(event.url)
|
||||||
|
) {
|
||||||
|
setIsSessionTrusted(true);
|
||||||
|
}
|
||||||
if (!title && event.title) {
|
if (!title && event.title) {
|
||||||
setPageTitle(event.title);
|
setPageTitle(event.title);
|
||||||
}
|
}
|
||||||
@@ -174,8 +410,8 @@ export const WebViewScreen: React.FC<WebViewScreenProps> = ({ route }) => {
|
|||||||
</ExpandableBottomLayout.TopSection>
|
</ExpandableBottomLayout.TopSection>
|
||||||
<ExpandableBottomLayout.BottomSection
|
<ExpandableBottomLayout.BottomSection
|
||||||
backgroundColor={white}
|
backgroundColor={white}
|
||||||
borderTopLeftRadius={30}
|
borderTopLeftRadius={20}
|
||||||
borderTopRightRadius={30}
|
borderTopRightRadius={20}
|
||||||
borderTopWidth={1}
|
borderTopWidth={1}
|
||||||
borderColor={slate200}
|
borderColor={slate200}
|
||||||
style={{ paddingTop: 0 }}
|
style={{ paddingTop: 0 }}
|
||||||
@@ -192,21 +428,3 @@ export const WebViewScreen: React.FC<WebViewScreenProps> = ({ route }) => {
|
|||||||
</ExpandableBottomLayout.Layout>
|
</ExpandableBottomLayout.Layout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
webViewContainer: {
|
|
||||||
flex: 1,
|
|
||||||
alignSelf: 'stretch',
|
|
||||||
backgroundColor: white,
|
|
||||||
},
|
|
||||||
webView: {
|
|
||||||
flex: 1,
|
|
||||||
backgroundColor: white,
|
|
||||||
},
|
|
||||||
loadingOverlay: {
|
|
||||||
...StyleSheet.absoluteFillObject,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
backgroundColor: 'rgba(255,255,255,0.5)',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -8,3 +8,4 @@
|
|||||||
* Use this constant instead of checking __DEV__ directly throughout the codebase.
|
* Use this constant instead of checking __DEV__ directly throughout the codebase.
|
||||||
*/
|
*/
|
||||||
export const IS_DEV_MODE = typeof __DEV__ !== 'undefined' && __DEV__;
|
export const IS_DEV_MODE = typeof __DEV__ !== 'undefined' && __DEV__;
|
||||||
|
export const IS_EUCLID_ENABLED = false; //IS_DEV_MODE; // just in case we forgot to turn it off before pushing to prod.
|
||||||
|
|||||||
@@ -9,6 +9,19 @@
|
|||||||
|
|
||||||
// Crypto utilities
|
// Crypto utilities
|
||||||
export type { ModalCallbacks } from '@/utils/modalCallbackRegistry';
|
export type { ModalCallbacks } from '@/utils/modalCallbackRegistry';
|
||||||
|
|
||||||
|
// WebView utilities
|
||||||
|
export type { WebViewRequestWithIosProps } from '@/utils/webview';
|
||||||
|
|
||||||
|
export {
|
||||||
|
DISALLOWED_SCHEMES,
|
||||||
|
TRUSTED_DOMAINS,
|
||||||
|
isAllowedAboutUrl,
|
||||||
|
isSameOrigin,
|
||||||
|
isTrustedDomain,
|
||||||
|
isUserInitiatedTopFrameNavigation,
|
||||||
|
} from '@/utils/webview';
|
||||||
|
|
||||||
// Format utilities
|
// Format utilities
|
||||||
export { IS_DEV_MODE } from '@/utils/devUtils';
|
export { IS_DEV_MODE } from '@/utils/devUtils';
|
||||||
|
|
||||||
|
|||||||
193
app/src/utils/webview.ts
Normal file
193
app/src/utils/webview.ts
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||||
|
// SPDX-License-Identifier: BUSL-1.1
|
||||||
|
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||||
|
|
||||||
|
import { Platform } from 'react-native';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebView request object with iOS-specific properties for navigation control.
|
||||||
|
* Used to determine if a navigation is user-initiated and from the top frame.
|
||||||
|
*/
|
||||||
|
export interface WebViewRequestWithIosProps {
|
||||||
|
isTopFrame?: boolean;
|
||||||
|
navigationType?:
|
||||||
|
| 'click'
|
||||||
|
| 'formsubmit'
|
||||||
|
| 'formresubmit'
|
||||||
|
| 'backforward'
|
||||||
|
| 'reload'
|
||||||
|
| 'other';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Domains that should always open externally (e.g., wallet popups that require
|
||||||
|
* a full browser context to maintain window.opener relationship).
|
||||||
|
*/
|
||||||
|
export const ALWAYS_OPEN_EXTERNALLY = Object.freeze([
|
||||||
|
'keys.coinbase.com',
|
||||||
|
]) as readonly string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schemes that are disallowed from being opened externally.
|
||||||
|
* Using a blacklist approach - block specific dangerous schemes, allow everything else.
|
||||||
|
* Includes both variants (with and without '://') to catch all forms of these schemes.
|
||||||
|
*/
|
||||||
|
export const DISALLOWED_SCHEMES = Object.freeze([
|
||||||
|
'ftp://',
|
||||||
|
'ftp:',
|
||||||
|
'ftps://',
|
||||||
|
'ftps:',
|
||||||
|
'file://',
|
||||||
|
'file:',
|
||||||
|
// eslint-disable-next-line no-script-url
|
||||||
|
'javascript:',
|
||||||
|
'data:',
|
||||||
|
'blob:',
|
||||||
|
]) as readonly string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trusted entrypoints: these domains are allowed to start a session.
|
||||||
|
* Once a session starts from a trusted domain, HTTPS child navigations are
|
||||||
|
* allowed without expanding this list (parent-trusted session model).
|
||||||
|
* This keeps partners from breaking the WebView when they add dependencies,
|
||||||
|
* while still requiring the initial navigation to be curated.
|
||||||
|
*
|
||||||
|
* Note: Domains in ALWAYS_OPEN_EXTERNALLY (e.g., keys.coinbase.com) are
|
||||||
|
* excluded from this list as they require full browser context and cannot
|
||||||
|
* be trusted WebView entrypoints.
|
||||||
|
*/
|
||||||
|
export const TRUSTED_DOMAINS = Object.freeze([
|
||||||
|
'aave.com', // Aave protocol - DeFi lending network
|
||||||
|
'amity-lock-11401309.figma.site', // Degen Tarot game
|
||||||
|
'celo.org', // CELO Names - includes names.celo.org
|
||||||
|
'cloud.google.com', // Google Cloud - AI agents in the cloud (includes cloud.google.com)
|
||||||
|
'coinbase.com', // Coinbase - Main domain
|
||||||
|
'karmahq.xyz', // Karma - Launch & fund projects
|
||||||
|
'lemonade.social', // Lemonade - Events and communities
|
||||||
|
'self.xyz', // Base domain and all subdomains (*.self.xyz) - includes espresso.self.xyz
|
||||||
|
'talent.app', // Talent Protocol - Main app
|
||||||
|
'talentprotocol.com', // Talent Protocol - Marketing/info site
|
||||||
|
'velodrome.finance', // Velodrome - Swap, deposit, take the lead
|
||||||
|
]) as readonly string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a URL is an allowed about: URL (about:blank or about:srcdoc).
|
||||||
|
* These URLs are allowed during trusted sessions for wallet bootstrap flows.
|
||||||
|
*/
|
||||||
|
export const isAllowedAboutUrl = (url: string): boolean => {
|
||||||
|
const lower = url.toLowerCase();
|
||||||
|
return lower === 'about:blank' || lower === 'about:srcdoc';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a URL's hostname matches a given domain (exact or subdomain match).
|
||||||
|
* Returns false for malformed URLs or if the URL doesn't match.
|
||||||
|
*
|
||||||
|
* @param url - The URL to check
|
||||||
|
* @param domain - The domain to match against (e.g., 'example.com')
|
||||||
|
* @returns true if hostname matches domain or is a subdomain of it
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* isHostnameMatch('https://example.com/path', 'example.com') // true
|
||||||
|
* isHostnameMatch('https://sub.example.com/path', 'example.com') // true
|
||||||
|
* isHostnameMatch('https://evil.com/?next=example.com', 'example.com') // false
|
||||||
|
*/
|
||||||
|
export const isHostnameMatch = (url: string, domain: string): boolean => {
|
||||||
|
try {
|
||||||
|
const hostname = new URL(url).hostname;
|
||||||
|
return hostname === domain || hostname.endsWith(`.${domain}`);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if two URLs have the same origin (protocol + host + port).
|
||||||
|
* Returns false for malformed URLs.
|
||||||
|
*/
|
||||||
|
export const isSameOrigin = (url1: string, url2: string): boolean => {
|
||||||
|
try {
|
||||||
|
return new URL(url1).origin === new URL(url2).origin;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a URL is from a trusted domain.
|
||||||
|
* Matches exact domain or any subdomain of trusted domains.
|
||||||
|
* Returns false for malformed URLs.
|
||||||
|
*
|
||||||
|
* Note: Domains in ALWAYS_OPEN_EXTERNALLY (e.g., keys.coinbase.com) are
|
||||||
|
* excluded even if they would match as subdomains of trusted domains.
|
||||||
|
*/
|
||||||
|
export const isTrustedDomain = (url: string): boolean => {
|
||||||
|
try {
|
||||||
|
const hostname = new URL(url).hostname;
|
||||||
|
|
||||||
|
// First check if this domain should always open externally
|
||||||
|
// These domains cannot be trusted entrypoints even if they're subdomains of trusted domains
|
||||||
|
const alwaysExternal = ALWAYS_OPEN_EXTERNALLY.some(
|
||||||
|
domain => hostname === domain || hostname.endsWith(`.${domain}`),
|
||||||
|
);
|
||||||
|
if (alwaysExternal) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then check if it matches any trusted domain
|
||||||
|
return TRUSTED_DOMAINS.some(
|
||||||
|
domain => hostname === domain || hostname.endsWith(`.${domain}`),
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* iOS-only mitigation for drive-by deep-linking via iframes.
|
||||||
|
* Gates external URL opens to top-frame, user-initiated navigations.
|
||||||
|
*
|
||||||
|
* On iOS, isTopFrame and navigationType are available on the request object.
|
||||||
|
* On Android, these properties are unavailable, so we allow all navigations.
|
||||||
|
*
|
||||||
|
* This prevents malicious iframes on trusted partner sites from invoking
|
||||||
|
* external app opens (sms:, mailto:, etc.) without explicit user interaction.
|
||||||
|
*/
|
||||||
|
export const isUserInitiatedTopFrameNavigation = (
|
||||||
|
req: WebViewRequestWithIosProps,
|
||||||
|
): boolean => {
|
||||||
|
// Android: these properties are unavailable, allow all navigations
|
||||||
|
if (Platform.OS !== 'ios') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// iOS: block if explicitly from an iframe
|
||||||
|
if (req.isTopFrame === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// iOS: only allow 'click' or undefined (backward compatibility) navigations
|
||||||
|
// Block 'other', 'reload', 'formsubmit', 'backforward' as non-user-initiated
|
||||||
|
const navType = req.navigationType;
|
||||||
|
if (navType !== undefined && navType !== 'click') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if a URL should always be opened externally.
|
||||||
|
* Used for special cases like Coinbase wallet that require window.opener.
|
||||||
|
* Returns false for malformed URLs.
|
||||||
|
*/
|
||||||
|
export const shouldAlwaysOpenExternally = (url: string): boolean => {
|
||||||
|
try {
|
||||||
|
const hostname = new URL(url).hostname;
|
||||||
|
return ALWAYS_OPEN_EXTERNALLY.some(
|
||||||
|
domain => hostname === domain || hostname.endsWith(`.${domain}`),
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -44,7 +44,7 @@ describe('parseScanResponse', () => {
|
|||||||
global.mockPlatformOS = 'ios';
|
global.mockPlatformOS = 'ios';
|
||||||
});
|
});
|
||||||
|
|
||||||
it('parses iOS response', () => {
|
it.skip('parses iOS response', () => {
|
||||||
// Platform.OS is already mocked as 'ios' by default
|
// Platform.OS is already mocked as 'ios' by default
|
||||||
const mrz =
|
const mrz =
|
||||||
'P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<L898902C<3UTO6908061F9406236ZE184226B<<<<<14';
|
'P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<L898902C<3UTO6908061F9406236ZE184226B<<<<<14';
|
||||||
@@ -61,8 +61,45 @@ describe('parseScanResponse', () => {
|
|||||||
passportPhoto: 'photo',
|
passportPhoto: 'photo',
|
||||||
documentSigningCertificate: JSON.stringify({ PEM: 'CERT' }),
|
documentSigningCertificate: JSON.stringify({ PEM: 'CERT' }),
|
||||||
});
|
});
|
||||||
|
expect(response).toMatchInlineSnapshot(
|
||||||
|
`"{"dataGroupHashes":"{\\"DG1\\":{\\"sodHash\\":\\"abcd\\"},\\"DG2\\":{\\"sodHash\\":\\"1234\\"}}","eContentBase64":"ZWM=","signedAttributes":"c2E=","passportMRZ":"P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<L898902C<3UTO6908061F9406236ZE184226B<<<<<14","signatureBase64":"AQI=","dataGroupsPresent":[1,2],"passportPhoto":"photo","documentSigningCertificate":"{\\"PEM\\":\\"CERT\\"}"}"`,
|
||||||
|
);
|
||||||
const result = parseScanResponse(response);
|
const result = parseScanResponse(response);
|
||||||
|
console.log('Parsed Result:', result);
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"dg1Hash": [
|
||||||
|
171,
|
||||||
|
205,
|
||||||
|
],
|
||||||
|
"dg2Hash": [
|
||||||
|
18,
|
||||||
|
52,
|
||||||
|
],
|
||||||
|
"dgPresents": [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
],
|
||||||
|
"documentCategory": "passport",
|
||||||
|
"documentType": "passport",
|
||||||
|
"dsc": "CERT",
|
||||||
|
"eContent": [
|
||||||
|
101,
|
||||||
|
99,
|
||||||
|
],
|
||||||
|
"encryptedDigest": [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
],
|
||||||
|
"mock": false,
|
||||||
|
"mrz": "P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<L898902C<3UTO6908061F9406236ZE184226B<<<<<14",
|
||||||
|
"parsed": false,
|
||||||
|
"signedAttr": [
|
||||||
|
115,
|
||||||
|
97,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`);
|
||||||
expect(result.mrz).toBe(mrz);
|
expect(result.mrz).toBe(mrz);
|
||||||
expect(result.documentType).toBe('passport');
|
expect(result.documentType).toBe('passport');
|
||||||
// 'abcd' in hex: ab = 171, cd = 205
|
// 'abcd' in hex: ab = 171, cd = 205
|
||||||
@@ -86,8 +123,50 @@ describe('parseScanResponse', () => {
|
|||||||
// Android format: '1' and '2' are hex strings, not arrays
|
// Android format: '1' and '2' are hex strings, not arrays
|
||||||
dataGroupHashes: JSON.stringify({ '1': 'abcd', '2': '1234' }),
|
dataGroupHashes: JSON.stringify({ '1': 'abcd', '2': '1234' }),
|
||||||
} as any;
|
} as any;
|
||||||
|
expect(response).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"dataGroupHashes": "{"1":"abcd","2":"1234"}",
|
||||||
|
"documentSigningCertificate": "CERT",
|
||||||
|
"eContent": "[4,5]",
|
||||||
|
"encapContent": "[8,9]",
|
||||||
|
"encryptedDigest": "[6,7]",
|
||||||
|
"mrz": "P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<L898902C<3UTO6908061F9406236ZE184226B<<<<<14",
|
||||||
|
}
|
||||||
|
`);
|
||||||
const result = parseScanResponse(response);
|
const result = parseScanResponse(response);
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"dg1Hash": [
|
||||||
|
171,
|
||||||
|
205,
|
||||||
|
],
|
||||||
|
"dg2Hash": [
|
||||||
|
18,
|
||||||
|
52,
|
||||||
|
],
|
||||||
|
"dgPresents": [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
],
|
||||||
|
"documentCategory": "passport",
|
||||||
|
"documentType": "passport",
|
||||||
|
"dsc": "-----BEGIN CERTIFICATE-----CERT-----END CERTIFICATE-----",
|
||||||
|
"eContent": [
|
||||||
|
8,
|
||||||
|
9,
|
||||||
|
],
|
||||||
|
"encryptedDigest": [
|
||||||
|
6,
|
||||||
|
7,
|
||||||
|
],
|
||||||
|
"mock": false,
|
||||||
|
"mrz": "P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<L898902C<3UTO6908061F9406236ZE184226B<<<<<14",
|
||||||
|
"signedAttr": [
|
||||||
|
4,
|
||||||
|
5,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`);
|
||||||
expect(result.documentType).toBe('passport');
|
expect(result.documentType).toBe('passport');
|
||||||
expect(result.mrz).toBe(mrz);
|
expect(result.mrz).toBe(mrz);
|
||||||
// 'abcd' in hex: ab = 171, cd = 205
|
// 'abcd' in hex: ab = 171, cd = 205
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ jest.mock('@/navigation', () => {
|
|||||||
// Documents screens
|
// Documents screens
|
||||||
IDPicker: {},
|
IDPicker: {},
|
||||||
IdDetails: {},
|
IdDetails: {},
|
||||||
CountryPicker: {},
|
CountryPicker: {
|
||||||
|
statusBar: { hidden: true, style: 'dark' },
|
||||||
|
},
|
||||||
DocumentCamera: {},
|
DocumentCamera: {},
|
||||||
DocumentCameraTrouble: {},
|
DocumentCameraTrouble: {},
|
||||||
DocumentDataInfo: {},
|
DocumentDataInfo: {},
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
258
app/tests/src/utils/webview.test.ts
Normal file
258
app/tests/src/utils/webview.test.ts
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||||
|
// SPDX-License-Identifier: BUSL-1.1
|
||||||
|
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||||
|
|
||||||
|
import {
|
||||||
|
ALWAYS_OPEN_EXTERNALLY,
|
||||||
|
isAllowedAboutUrl,
|
||||||
|
isHostnameMatch,
|
||||||
|
isSameOrigin,
|
||||||
|
isTrustedDomain,
|
||||||
|
shouldAlwaysOpenExternally,
|
||||||
|
TRUSTED_DOMAINS,
|
||||||
|
} from '@/utils/webview';
|
||||||
|
|
||||||
|
describe('webview utilities', () => {
|
||||||
|
describe('isHostnameMatch', () => {
|
||||||
|
it('should match exact domain', () => {
|
||||||
|
expect(isHostnameMatch('https://example.com', 'example.com')).toBe(true);
|
||||||
|
expect(isHostnameMatch('https://example.com/', 'example.com')).toBe(true);
|
||||||
|
expect(isHostnameMatch('https://example.com/path', 'example.com')).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
isHostnameMatch('https://example.com/path?query=1', 'example.com'),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should match subdomains', () => {
|
||||||
|
expect(isHostnameMatch('https://sub.example.com', 'example.com')).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
isHostnameMatch('https://sub.sub.example.com', 'example.com'),
|
||||||
|
).toBe(true);
|
||||||
|
expect(isHostnameMatch('https://www.example.com', 'example.com')).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should NOT match domain in query parameters (spoofing attempt)', () => {
|
||||||
|
expect(
|
||||||
|
isHostnameMatch('https://evil.com/?next=example.com', 'example.com'),
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
isHostnameMatch(
|
||||||
|
'https://evil.com/path?redirect=example.com',
|
||||||
|
'example.com',
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
isHostnameMatch('https://attacker.com#example.com', 'example.com'),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should NOT match domain in path (spoofing attempt)', () => {
|
||||||
|
expect(
|
||||||
|
isHostnameMatch('https://evil.com/example.com', 'example.com'),
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
isHostnameMatch('https://evil.com/path/example.com', 'example.com'),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should NOT match similar but different domains', () => {
|
||||||
|
expect(isHostnameMatch('https://example.org', 'example.com')).toBe(false);
|
||||||
|
expect(isHostnameMatch('https://notexample.com', 'example.com')).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
isHostnameMatch('https://example.com.evil.com', 'example.com'),
|
||||||
|
).toBe(false);
|
||||||
|
expect(isHostnameMatch('https://fakeexample.com', 'example.com')).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle malformed URLs gracefully', () => {
|
||||||
|
expect(isHostnameMatch('not a url', 'example.com')).toBe(false);
|
||||||
|
expect(isHostnameMatch('', 'example.com')).toBe(false);
|
||||||
|
// eslint-disable-next-line no-script-url
|
||||||
|
expect(isHostnameMatch('javascript:alert(1)', 'example.com')).toBe(false);
|
||||||
|
expect(isHostnameMatch('ftp://example.com', 'example.com')).toBe(true); // valid URL, different protocol
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be case-insensitive for hostnames', () => {
|
||||||
|
expect(isHostnameMatch('https://Example.COM', 'example.com')).toBe(true);
|
||||||
|
expect(isHostnameMatch('https://EXAMPLE.COM', 'example.com')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('WalletConnect spoofing protection', () => {
|
||||||
|
it('should match legitimate WalletConnect URLs', () => {
|
||||||
|
expect(
|
||||||
|
isHostnameMatch(
|
||||||
|
'https://verify.walletconnect.org/v3/attestation',
|
||||||
|
'verify.walletconnect.org',
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
isHostnameMatch(
|
||||||
|
'https://verify.walletconnect.org/path?query=1',
|
||||||
|
'verify.walletconnect.org',
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should NOT match spoofed WalletConnect URLs', () => {
|
||||||
|
expect(
|
||||||
|
isHostnameMatch(
|
||||||
|
'https://evil.com/?next=verify.walletconnect.org',
|
||||||
|
'verify.walletconnect.org',
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
isHostnameMatch(
|
||||||
|
'https://evil.com/verify.walletconnect.org',
|
||||||
|
'verify.walletconnect.org',
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
isHostnameMatch(
|
||||||
|
'https://verify.walletconnect.org.evil.com',
|
||||||
|
'verify.walletconnect.org',
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Aave spoofing protection', () => {
|
||||||
|
it('should match legitimate Aave URLs', () => {
|
||||||
|
expect(isHostnameMatch('https://app.aave.com', 'app.aave.com')).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
isHostnameMatch('https://app.aave.com/markets', 'app.aave.com'),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should NOT match spoofed Aave URLs', () => {
|
||||||
|
expect(
|
||||||
|
isHostnameMatch(
|
||||||
|
'https://evil.com/?redirect=app.aave.com',
|
||||||
|
'app.aave.com',
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
isHostnameMatch('https://evil.com/app.aave.com', 'app.aave.com'),
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
isHostnameMatch('https://app.aave.com.evil.com', 'app.aave.com'),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isTrustedDomain', () => {
|
||||||
|
it('should match domains from TRUSTED_DOMAINS list', () => {
|
||||||
|
TRUSTED_DOMAINS.forEach(domain => {
|
||||||
|
expect(isTrustedDomain(`https://${domain}`)).toBe(true);
|
||||||
|
expect(isTrustedDomain(`https://www.${domain}`)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not match untrusted domains', () => {
|
||||||
|
expect(isTrustedDomain('https://evil.com')).toBe(false);
|
||||||
|
expect(isTrustedDomain('https://attacker.org')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('shouldAlwaysOpenExternally', () => {
|
||||||
|
it('should match domains from ALWAYS_OPEN_EXTERNALLY list', () => {
|
||||||
|
ALWAYS_OPEN_EXTERNALLY.forEach(domain => {
|
||||||
|
expect(shouldAlwaysOpenExternally(`https://${domain}`)).toBe(true);
|
||||||
|
expect(shouldAlwaysOpenExternally(`https://www.${domain}`)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not match other domains', () => {
|
||||||
|
expect(shouldAlwaysOpenExternally('https://example.com')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Policy: keys.coinbase.com always opens externally', () => {
|
||||||
|
it('should be in ALWAYS_OPEN_EXTERNALLY list', () => {
|
||||||
|
expect(ALWAYS_OPEN_EXTERNALLY).toContain('keys.coinbase.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should NOT be in TRUSTED_DOMAINS list (policy conflict prevention)', () => {
|
||||||
|
expect(TRUSTED_DOMAINS).not.toContain('keys.coinbase.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for shouldAlwaysOpenExternally', () => {
|
||||||
|
expect(shouldAlwaysOpenExternally('https://keys.coinbase.com')).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
shouldAlwaysOpenExternally('https://keys.coinbase.com/connect'),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
shouldAlwaysOpenExternally('https://keys.coinbase.com/path?query=1'),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for isTrustedDomain', () => {
|
||||||
|
expect(isTrustedDomain('https://keys.coinbase.com')).toBe(false);
|
||||||
|
expect(isTrustedDomain('https://keys.coinbase.com/connect')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly identify that keys.coinbase.com cannot be a trusted entrypoint', () => {
|
||||||
|
// Verify the policy: if a domain should always open externally,
|
||||||
|
// it cannot be trusted in the WebView
|
||||||
|
const url = 'https://keys.coinbase.com/wallet';
|
||||||
|
expect(shouldAlwaysOpenExternally(url)).toBe(true);
|
||||||
|
expect(isTrustedDomain(url)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isAllowedAboutUrl', () => {
|
||||||
|
it('should allow about:blank and about:srcdoc', () => {
|
||||||
|
expect(isAllowedAboutUrl('about:blank')).toBe(true);
|
||||||
|
expect(isAllowedAboutUrl('about:srcdoc')).toBe(true);
|
||||||
|
expect(isAllowedAboutUrl('ABOUT:BLANK')).toBe(true);
|
||||||
|
expect(isAllowedAboutUrl('ABOUT:SRCDOC')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not allow other about: URLs', () => {
|
||||||
|
expect(isAllowedAboutUrl('about:config')).toBe(false);
|
||||||
|
expect(isAllowedAboutUrl('about:plugins')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isSameOrigin', () => {
|
||||||
|
it('should return true for same origin URLs', () => {
|
||||||
|
expect(
|
||||||
|
isSameOrigin('https://example.com/path1', 'https://example.com/path2'),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
isSameOrigin('https://example.com:443/', 'https://example.com/'),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for different origins', () => {
|
||||||
|
expect(isSameOrigin('https://example.com', 'https://other.com')).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
expect(isSameOrigin('https://example.com', 'http://example.com')).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
isSameOrigin('https://example.com:443', 'https://example.com:8443'),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle malformed URLs gracefully', () => {
|
||||||
|
expect(isSameOrigin('not a url', 'https://example.com')).toBe(false);
|
||||||
|
expect(isSameOrigin('https://example.com', 'not a url')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"ios": {
|
"ios": {
|
||||||
"build": 192,
|
"build": 193,
|
||||||
"lastDeployed": "2025-12-05T00:06:05.459Z"
|
"lastDeployed": "2025-12-06T09:48:56.530Z"
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
"build": 123,
|
"build": 124,
|
||||||
"lastDeployed": "2025-11-21T00:06:05.459Z"
|
"lastDeployed": "2025-12-06T09:48:56.530Z"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,7 +113,16 @@ template REGISTER_AADHAAR(n, k, maxDataLength){
|
|||||||
signal output nullifier <== nullifierHasher.out;
|
signal output nullifier <== nullifierHasher.out;
|
||||||
|
|
||||||
|
|
||||||
signal qrDataHash <== PackBytesAndPoseidon(maxDataLength)(qrDataPadded);
|
component qrDataHasher = PackBytesAndPoseidon(maxDataLength);
|
||||||
|
for (var i = 0; i < 9; i++){
|
||||||
|
qrDataHasher.in[i] <== qrDataPadded[i];
|
||||||
|
}
|
||||||
|
for (var i = 9; i < 26; i++) {
|
||||||
|
qrDataHasher.in[i] <== 0;
|
||||||
|
}
|
||||||
|
for (var i = 26; i < maxDataLength; i++){
|
||||||
|
qrDataHasher.in[i] <== qrDataPadded[i];
|
||||||
|
}
|
||||||
|
|
||||||
// Generate commitment
|
// Generate commitment
|
||||||
component packedCommitment = PackBytesAndPoseidon(42 + 62);
|
component packedCommitment = PackBytesAndPoseidon(42 + 62);
|
||||||
@@ -138,7 +147,7 @@ template REGISTER_AADHAAR(n, k, maxDataLength){
|
|||||||
component commitmentHasher = Poseidon(5);
|
component commitmentHasher = Poseidon(5);
|
||||||
|
|
||||||
commitmentHasher.inputs[0] <== secret;
|
commitmentHasher.inputs[0] <== secret;
|
||||||
commitmentHasher.inputs[1] <== qrDataHash;
|
commitmentHasher.inputs[1] <== qrDataHasher.out;
|
||||||
commitmentHasher.inputs[2] <== nullifierHasher.out;
|
commitmentHasher.inputs[2] <== nullifierHasher.out;
|
||||||
commitmentHasher.inputs[3] <== packedCommitment.out;
|
commitmentHasher.inputs[3] <== packedCommitment.out;
|
||||||
commitmentHasher.inputs[4] <== qrDataExtractor.photoHash;
|
commitmentHasher.inputs[4] <== qrDataExtractor.photoHash;
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ export const OFAC_TREE_LEVELS = 64;
|
|||||||
// we make it global here because passing it to generateCircuitInputsRegister caused trouble
|
// we make it global here because passing it to generateCircuitInputsRegister caused trouble
|
||||||
export const PASSPORT_ATTESTATION_ID = '1';
|
export const PASSPORT_ATTESTATION_ID = '1';
|
||||||
|
|
||||||
export const PCR0_MANAGER_ADDRESS = '0xE36d4EE5Fd3916e703A46C21Bb3837dB7680C8B8';
|
export const PCR0_MANAGER_ADDRESS = '0x9743fe2C1c3D2b068c56dE314e9B10DA9c904717';
|
||||||
|
|
||||||
export const REDIRECT_URL = 'https://redirect.self.xyz';
|
export const REDIRECT_URL = 'https://redirect.self.xyz';
|
||||||
|
|
||||||
|
|||||||
@@ -208,8 +208,6 @@ export function prepareAadhaarDiscloseData(
|
|||||||
secret,
|
secret,
|
||||||
qrDataHash: formatInput(BigInt(sharedData.qrHash)),
|
qrDataHash: formatInput(BigInt(sharedData.qrHash)),
|
||||||
gender: formatInput(genderAscii),
|
gender: formatInput(genderAscii),
|
||||||
// qrDataHash: BigInt(sharedData.qrHash).toString(),
|
|
||||||
// gender: genderAscii.toString(),
|
|
||||||
yob: stringToAsciiArray(sharedData.extractedFields.yob),
|
yob: stringToAsciiArray(sharedData.extractedFields.yob),
|
||||||
mob: stringToAsciiArray(sharedData.extractedFields.mob),
|
mob: stringToAsciiArray(sharedData.extractedFields.mob),
|
||||||
dob: stringToAsciiArray(sharedData.extractedFields.dob),
|
dob: stringToAsciiArray(sharedData.extractedFields.dob),
|
||||||
@@ -551,7 +549,6 @@ export function processQRData(
|
|||||||
QRData = newTestData.testQRData;
|
QRData = newTestData.testQRData;
|
||||||
} else {
|
} else {
|
||||||
QRData = testQRData.testQRData;
|
QRData = testQRData.testQRData;
|
||||||
// console.log('testQRData:', testQRData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return processQRDataSimple(QRData);
|
return processQRDataSimple(QRData);
|
||||||
@@ -576,6 +573,13 @@ export function processQRDataSimple(qrData: string) {
|
|||||||
// Extract actual fields from QR data instead of using hardcoded values
|
// Extract actual fields from QR data instead of using hardcoded values
|
||||||
const extractedFields = extractQRDataFields(qrDataBytes);
|
const extractedFields = extractQRDataFields(qrDataBytes);
|
||||||
|
|
||||||
|
// Calculate qrHash exclude timestamp (positions 9-25, 17 bytes)
|
||||||
|
// const qrDataWithoutTimestamp = [
|
||||||
|
// ...Array.from(qrDataPadded.slice(0, 9)),
|
||||||
|
// ...Array.from(qrDataPadded.slice(9, 26)).map((x) => 0),
|
||||||
|
// ...Array.from(qrDataPadded.slice(26)),
|
||||||
|
// ];
|
||||||
|
// const qrHash = packBytesAndPoseidon(qrDataWithoutTimestamp);
|
||||||
const qrHash = packBytesAndPoseidon(Array.from(qrDataPadded));
|
const qrHash = packBytesAndPoseidon(Array.from(qrDataPadded));
|
||||||
const photo = extractPhoto(Array.from(qrDataPadded), photoEOI + 1);
|
const photo = extractPhoto(Array.from(qrDataPadded), photoEOI + 1);
|
||||||
|
|
||||||
|
|||||||
@@ -398,7 +398,7 @@ export async function getAadharRegistrationWindow() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function returnNewDateString(timestamp?: string): string {
|
export function returnNewDateString(timestamp?: string): string {
|
||||||
const newDate = timestamp ? new Date(+timestamp) : new Date();
|
const newDate = timestamp ? new Date(+timestamp * 1000) : new Date();
|
||||||
|
|
||||||
// Convert the UTC date to IST by adding 5 hours and 30 minutes
|
// Convert the UTC date to IST by adding 5 hours and 30 minutes
|
||||||
const offsetHours = 5;
|
const offsetHours = 5;
|
||||||
|
|||||||
@@ -87,21 +87,19 @@ function compareCertificates(cert1: forge.pki.Certificate, cert2: forge.pki.Cert
|
|||||||
}
|
}
|
||||||
|
|
||||||
function verifyCertificateChain({ leaf, intermediate, root }: PKICertificates) {
|
function verifyCertificateChain({ leaf, intermediate, root }: PKICertificates) {
|
||||||
const caStore = forge.pki.createCaStore([intermediate, root]);
|
const caStore = forge.pki.createCaStore([root]);
|
||||||
|
|
||||||
forge.pki.verifyCertificateChain(caStore, [leaf], (vfd, depth, chain) => {
|
forge.pki.verifyCertificateChain(caStore, [leaf, intermediate, root], (vfd, depth) => {
|
||||||
if (!vfd) {
|
if (vfd !== true) {
|
||||||
throw new Error(`Certificate verification failed at depth ${depth}`);
|
throw new Error(`Certificate verification failed at depth ${depth}`);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
[leaf, intermediate, root].forEach((cert) => {
|
const now = new Date();
|
||||||
const now = new Date();
|
if (now < root.validity.notBefore || now > root.validity.notAfter) {
|
||||||
if (now < cert.validity.notBefore || now > cert.validity.notAfter) {
|
throw new Error('Certificate is not within validity period');
|
||||||
throw new Error('Certificate is not within validity period');
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -8,3 +8,6 @@ CELO_RPC_URL=https://celo.drpc.org
|
|||||||
CELO_SEPOLIA_RPC_URL=https://rpc.ankr.com/celo_sepolia
|
CELO_SEPOLIA_RPC_URL=https://rpc.ankr.com/celo_sepolia
|
||||||
|
|
||||||
ETHERSCAN_API_KEY=
|
ETHERSCAN_API_KEY=
|
||||||
|
|
||||||
|
STANDARD_GOVERNANCE_ADDRESS=
|
||||||
|
CRITICAL_GOVERNANCE_ADDRESS=
|
||||||
|
|||||||
214
contracts/UPGRADE_GUIDE.md
Normal file
214
contracts/UPGRADE_GUIDE.md
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
# Contract Upgrade Guide
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. Update Your Contract
|
||||||
|
|
||||||
|
```solidity
|
||||||
|
// Update version in NatSpec
|
||||||
|
* @custom:version 2.13.0
|
||||||
|
|
||||||
|
// Update reinitializer modifier (increment by 1)
|
||||||
|
function initialize(...) external reinitializer(13) {
|
||||||
|
// Add any new initialization logic
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Run the Upgrade Script
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd contracts
|
||||||
|
npx hardhat upgrade --contract IdentityVerificationHub --network celo --changelog "Added feature X"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Approve in Safe
|
||||||
|
|
||||||
|
The script outputs instructions to submit to the Safe multisig. Once 3/5 signers approve, execute the transaction.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Governance Roles
|
||||||
|
|
||||||
|
| Role | Threshold | Purpose |
|
||||||
|
| ----------------- | --------- | ------------------------------------ |
|
||||||
|
| `SECURITY_ROLE` | 3/5 | Contract upgrades, role management |
|
||||||
|
| `OPERATIONS_ROLE` | 2/5 | CSCA root updates, OFAC list updates |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Detailed Workflow
|
||||||
|
|
||||||
|
### Step 1: Modify the Contract
|
||||||
|
|
||||||
|
1. Make your code changes
|
||||||
|
2. Update `@custom:version` in the contract's NatSpec comment
|
||||||
|
3. Increment the `reinitializer(N)` modifier (e.g., `reinitializer(12)` → `reinitializer(13)`)
|
||||||
|
4. Add any new storage fields **at the end** of the storage struct
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```solidity
|
||||||
|
/**
|
||||||
|
* @title IdentityVerificationHubImplV2
|
||||||
|
* @custom:version 2.13.0
|
||||||
|
*/
|
||||||
|
contract IdentityVerificationHubImplV2 is ImplRoot {
|
||||||
|
|
||||||
|
struct HubStorage {
|
||||||
|
// Existing fields...
|
||||||
|
uint256 newField; // Add new fields at the end only
|
||||||
|
}
|
||||||
|
|
||||||
|
function initialize(...) external reinitializer(13) {
|
||||||
|
// Initialize new fields if needed
|
||||||
|
HubStorage storage $ = _getHubStorage();
|
||||||
|
$.newField = defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Run the Upgrade Script
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx hardhat upgrade --contract <ContractName> --network <network> --changelog "Description"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
|
||||||
|
- `--contract` - Contract name (e.g., `IdentityVerificationHub`)
|
||||||
|
- `--network` - Target network (`celo`, `sepolia`, `localhost`)
|
||||||
|
- `--changelog` - Brief description of changes
|
||||||
|
- `--prepare-only` - Deploy implementation without creating Safe proposal
|
||||||
|
|
||||||
|
### Step 3: Script Execution
|
||||||
|
|
||||||
|
The script automatically:
|
||||||
|
|
||||||
|
1. **Validates version** - Ensures `@custom:version` is incremented correctly
|
||||||
|
2. **Checks reinitializer** - Verifies `reinitializer(N)` matches expected version
|
||||||
|
3. **Validates storage** - Ensures no breaking storage layout changes
|
||||||
|
4. **Compiles fresh** - Clears cache to prevent stale bytecode
|
||||||
|
5. **Compares bytecode** - Warns if implementation hasn't changed
|
||||||
|
6. **Deploys implementation** - Deploys new implementation contract
|
||||||
|
7. **Updates registry** - Records deployment in `deployments/registry.json`
|
||||||
|
8. **Creates git commit & tag** - Auto-commits changes with version tag
|
||||||
|
9. **Creates Safe proposal** - If you're a signer, auto-proposes to Safe
|
||||||
|
|
||||||
|
### Step 4: Multisig Approval
|
||||||
|
|
||||||
|
**If you're a Safe signer:**
|
||||||
|
|
||||||
|
- Script auto-proposes the transaction
|
||||||
|
- Other signers approve in Safe UI
|
||||||
|
- Execute once threshold (3/5) is met
|
||||||
|
|
||||||
|
**If you're not a signer:**
|
||||||
|
|
||||||
|
- Script outputs transaction data for manual submission
|
||||||
|
- Copy data to Safe Transaction Builder
|
||||||
|
- Signers approve and execute
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Safety Checks
|
||||||
|
|
||||||
|
The upgrade script performs these automatic checks:
|
||||||
|
|
||||||
|
| Check | What it Does | Failure Behavior |
|
||||||
|
| ---------------------- | ---------------------------------- | -------------------- |
|
||||||
|
| Version validation | Ensures semantic version increment | Blocks upgrade |
|
||||||
|
| Reinitializer check | Verifies modifier matches version | Blocks upgrade |
|
||||||
|
| Storage layout | Detects breaking storage changes | Blocks upgrade |
|
||||||
|
| Bytecode comparison | Warns if code unchanged | Prompts confirmation |
|
||||||
|
| Safe role verification | Confirms Safe has `SECURITY_ROLE` | Blocks upgrade |
|
||||||
|
| Constructor check | Flags `_disableInitializers()` | Prompts confirmation |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Registry Structure
|
||||||
|
|
||||||
|
All deployments are tracked in `deployments/registry.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"contracts": {
|
||||||
|
"ContractName": {
|
||||||
|
"source": "ContractSourceFile",
|
||||||
|
"type": "uups-proxy"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"networks": {
|
||||||
|
"celo": {
|
||||||
|
"deployments": {
|
||||||
|
"ContractName": {
|
||||||
|
"proxy": "0x...",
|
||||||
|
"currentVersion": "2.12.0",
|
||||||
|
"currentImpl": "0x..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"versions": {
|
||||||
|
"ContractName": {
|
||||||
|
"2.12.0": {
|
||||||
|
"initializerVersion": 12,
|
||||||
|
"changelog": "...",
|
||||||
|
"gitTag": "contractname-v2.12.0",
|
||||||
|
"deployments": { ... }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Utility Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check current deployment status
|
||||||
|
npx hardhat upgrade:status --contract IdentityVerificationHub --network celo
|
||||||
|
|
||||||
|
# View version history
|
||||||
|
npx hardhat upgrade:history --contract IdentityVerificationHub
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rollback
|
||||||
|
|
||||||
|
If issues occur after upgrade:
|
||||||
|
|
||||||
|
1. Deploy the previous implementation version
|
||||||
|
2. Create Safe transaction calling `upgradeToAndCall(previousImpl, "0x")`
|
||||||
|
3. Execute with 3/5 multisig approval
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Environment Setup
|
||||||
|
|
||||||
|
Required in `.env`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CELO_RPC_URL=https://forno.celo.org
|
||||||
|
PRIVATE_KEY=0x... # Deployer wallet (needs ETH for gas)
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional for contract verification:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CELOSCAN_API_KEY=...
|
||||||
|
ETHERSCAN_API_KEY=...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Issue | Solution |
|
||||||
|
| ----------------------------- | ----------------------------------------- |
|
||||||
|
| "Version matches current" | Update `@custom:version` in contract |
|
||||||
|
| "Reinitializer mismatch" | Update `reinitializer(N)` to next version |
|
||||||
|
| "Storage layout incompatible" | Don't remove/reorder storage variables |
|
||||||
|
| "Safe not indexed" | Submit manually via Safe UI |
|
||||||
|
| "Bytecode unchanged" | Ensure you saved contract changes |
|
||||||
@@ -12,7 +12,6 @@ import {IIdentityRegistryV1} from "./interfaces/IIdentityRegistryV1.sol";
|
|||||||
import {IRegisterCircuitVerifier} from "./interfaces/IRegisterCircuitVerifier.sol";
|
import {IRegisterCircuitVerifier} from "./interfaces/IRegisterCircuitVerifier.sol";
|
||||||
import {IVcAndDiscloseCircuitVerifier} from "./interfaces/IVcAndDiscloseCircuitVerifier.sol";
|
import {IVcAndDiscloseCircuitVerifier} from "./interfaces/IVcAndDiscloseCircuitVerifier.sol";
|
||||||
import {IDscCircuitVerifier} from "./interfaces/IDscCircuitVerifier.sol";
|
import {IDscCircuitVerifier} from "./interfaces/IDscCircuitVerifier.sol";
|
||||||
import {ImplRoot} from "./upgradeable/ImplRoot.sol";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @notice ⚠️ CRITICAL STORAGE LAYOUT WARNING ⚠️
|
* @notice ⚠️ CRITICAL STORAGE LAYOUT WARNING ⚠️
|
||||||
@@ -43,9 +42,12 @@ import {ImplRoot} from "./upgradeable/ImplRoot.sol";
|
|||||||
/**
|
/**
|
||||||
* @title IdentityVerificationHubStorageV1
|
* @title IdentityVerificationHubStorageV1
|
||||||
* @notice Storage contract for IdentityVerificationHubImplV1.
|
* @notice Storage contract for IdentityVerificationHubImplV1.
|
||||||
* @dev Inherits from ImplRoot to include upgradeability functionality.
|
* @dev Inherits from UUPSUpgradeable and Ownable2StepUpgradeable to include upgradeability functionality.
|
||||||
*/
|
*/
|
||||||
abstract contract IdentityVerificationHubStorageV1 is ImplRoot {
|
abstract contract IdentityVerificationHubStorageV1 is UUPSUpgradeable, Ownable2StepUpgradeable {
|
||||||
|
// Reserved storage space to allow for layout changes in the future.
|
||||||
|
uint256[50] private __gap;
|
||||||
|
|
||||||
// ====================================================
|
// ====================================================
|
||||||
// Storage Variables
|
// Storage Variables
|
||||||
// ====================================================
|
// ====================================================
|
||||||
@@ -61,6 +63,14 @@ abstract contract IdentityVerificationHubStorageV1 is ImplRoot {
|
|||||||
|
|
||||||
/// @notice Mapping from signature type to DSC circuit verifier addresses..
|
/// @notice Mapping from signature type to DSC circuit verifier addresses..
|
||||||
mapping(uint256 => address) internal _sigTypeToDscCircuitVerifiers;
|
mapping(uint256 => address) internal _sigTypeToDscCircuitVerifiers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Authorizes an upgrade to a new implementation.
|
||||||
|
* Requirements:
|
||||||
|
* - Must be called through a proxy.
|
||||||
|
* - Caller must be the owner.
|
||||||
|
*/
|
||||||
|
function _authorizeUpgrade(address newImplementation) internal virtual override onlyProxy onlyOwner {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -207,7 +217,7 @@ contract IdentityVerificationHubImplV1 is IdentityVerificationHubStorageV1, IIde
|
|||||||
uint256[] memory dscCircuitVerifierIds,
|
uint256[] memory dscCircuitVerifierIds,
|
||||||
address[] memory dscCircuitVerifierAddresses
|
address[] memory dscCircuitVerifierAddresses
|
||||||
) external initializer {
|
) external initializer {
|
||||||
__ImplRoot_init();
|
__Ownable_init(msg.sender);
|
||||||
_registry = registryAddress;
|
_registry = registryAddress;
|
||||||
_vcAndDiscloseCircuitVerifier = vcAndDiscloseCircuitVerifierAddress;
|
_vcAndDiscloseCircuitVerifier = vcAndDiscloseCircuitVerifierAddress;
|
||||||
if (registerCircuitVerifierIds.length != registerCircuitVerifierAddresses.length) {
|
if (registerCircuitVerifierIds.length != registerCircuitVerifierAddresses.length) {
|
||||||
|
|||||||
@@ -19,6 +19,14 @@ import {IDscCircuitVerifier} from "./interfaces/IDscCircuitVerifier.sol";
|
|||||||
import {CircuitConstantsV2} from "./constants/CircuitConstantsV2.sol";
|
import {CircuitConstantsV2} from "./constants/CircuitConstantsV2.sol";
|
||||||
import {Formatter} from "./libraries/Formatter.sol";
|
import {Formatter} from "./libraries/Formatter.sol";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @title IdentityVerificationHubImplV2
|
||||||
|
* @notice Main hub for identity verification in the Self Protocol
|
||||||
|
* @dev This contract orchestrates multi-step verification processes including document attestation,
|
||||||
|
* zero-knowledge proofs, OFAC compliance, and attribute disclosure control.
|
||||||
|
*
|
||||||
|
* @custom:version 2.12.0
|
||||||
|
*/
|
||||||
contract IdentityVerificationHubImplV2 is ImplRoot {
|
contract IdentityVerificationHubImplV2 is ImplRoot {
|
||||||
/// @custom:storage-location erc7201:self.storage.IdentityVerificationHub
|
/// @custom:storage-location erc7201:self.storage.IdentityVerificationHub
|
||||||
struct IdentityVerificationHubStorage {
|
struct IdentityVerificationHubStorage {
|
||||||
@@ -45,7 +53,7 @@ contract IdentityVerificationHubImplV2 is ImplRoot {
|
|||||||
0xf9b5980dcec1a8b0609576a1f453bb2cad4732a0ea02bb89154d44b14a306c00;
|
0xf9b5980dcec1a8b0609576a1f453bb2cad4732a0ea02bb89154d44b14a306c00;
|
||||||
|
|
||||||
/// @notice The AADHAAR registration window around the current block timestamp.
|
/// @notice The AADHAAR registration window around the current block timestamp.
|
||||||
uint256 public AADHAAR_REGISTRATION_WINDOW = 20;
|
uint256 public AADHAAR_REGISTRATION_WINDOW;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @notice Returns the storage struct for the main IdentityVerificationHub.
|
* @notice Returns the storage struct for the main IdentityVerificationHub.
|
||||||
@@ -218,6 +226,7 @@ contract IdentityVerificationHubImplV2 is ImplRoot {
|
|||||||
* @notice Constructor that disables initializers for the implementation contract.
|
* @notice Constructor that disables initializers for the implementation contract.
|
||||||
* @dev This prevents the implementation contract from being initialized directly.
|
* @dev This prevents the implementation contract from being initialized directly.
|
||||||
* The actual initialization should only happen through the proxy.
|
* The actual initialization should only happen through the proxy.
|
||||||
|
* @custom:oz-upgrades-unsafe-allow constructor
|
||||||
*/
|
*/
|
||||||
constructor() {
|
constructor() {
|
||||||
_disableInitializers();
|
_disableInitializers();
|
||||||
@@ -240,9 +249,25 @@ contract IdentityVerificationHubImplV2 is ImplRoot {
|
|||||||
IdentityVerificationHubStorage storage $ = _getIdentityVerificationHubStorage();
|
IdentityVerificationHubStorage storage $ = _getIdentityVerificationHubStorage();
|
||||||
$._circuitVersion = 2;
|
$._circuitVersion = 2;
|
||||||
|
|
||||||
|
// Initialize Aadhaar registration window
|
||||||
|
AADHAAR_REGISTRATION_WINDOW = 20;
|
||||||
|
|
||||||
emit HubInitializedV2();
|
emit HubInitializedV2();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Initializes governance for upgraded contracts.
|
||||||
|
* @dev Used when upgrading from Ownable to AccessControl governance.
|
||||||
|
* This function sets up AccessControl roles on an already-initialized contract.
|
||||||
|
* It does NOT modify existing state (hub, roots, etc.).
|
||||||
|
*
|
||||||
|
* SECURITY: This function can only be called once - enforced by reinitializer(12).
|
||||||
|
* The previous version used reinitializer(11), so this upgrade uses version 12.
|
||||||
|
*/
|
||||||
|
function initializeGovernance() external reinitializer(12) {
|
||||||
|
__ImplRoot_init();
|
||||||
|
}
|
||||||
|
|
||||||
// ====================================================
|
// ====================================================
|
||||||
// External Functions
|
// External Functions
|
||||||
// ====================================================
|
// ====================================================
|
||||||
@@ -329,7 +354,7 @@ contract IdentityVerificationHubImplV2 is ImplRoot {
|
|||||||
* @notice Updates the AADHAAR registration window.
|
* @notice Updates the AADHAAR registration window.
|
||||||
* @param window The new AADHAAR registration window.
|
* @param window The new AADHAAR registration window.
|
||||||
*/
|
*/
|
||||||
function setAadhaarRegistrationWindow(uint256 window) external virtual onlyProxy onlyOwner {
|
function setAadhaarRegistrationWindow(uint256 window) external virtual onlyProxy onlyRole(SECURITY_ROLE) {
|
||||||
AADHAAR_REGISTRATION_WINDOW = window;
|
AADHAAR_REGISTRATION_WINDOW = window;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,7 +397,10 @@ contract IdentityVerificationHubImplV2 is ImplRoot {
|
|||||||
* @notice Updates the registry address.
|
* @notice Updates the registry address.
|
||||||
* @param registryAddress The new registry address.
|
* @param registryAddress The new registry address.
|
||||||
*/
|
*/
|
||||||
function updateRegistry(bytes32 attestationId, address registryAddress) external virtual onlyProxy onlyOwner {
|
function updateRegistry(
|
||||||
|
bytes32 attestationId,
|
||||||
|
address registryAddress
|
||||||
|
) external virtual onlyProxy onlyRole(SECURITY_ROLE) {
|
||||||
IdentityVerificationHubStorage storage $ = _getIdentityVerificationHubStorage();
|
IdentityVerificationHubStorage storage $ = _getIdentityVerificationHubStorage();
|
||||||
$._registries[attestationId] = registryAddress;
|
$._registries[attestationId] = registryAddress;
|
||||||
emit RegistryUpdated(attestationId, registryAddress);
|
emit RegistryUpdated(attestationId, registryAddress);
|
||||||
@@ -385,7 +413,7 @@ contract IdentityVerificationHubImplV2 is ImplRoot {
|
|||||||
function updateVcAndDiscloseCircuit(
|
function updateVcAndDiscloseCircuit(
|
||||||
bytes32 attestationId,
|
bytes32 attestationId,
|
||||||
address vcAndDiscloseCircuitVerifierAddress
|
address vcAndDiscloseCircuitVerifierAddress
|
||||||
) external virtual onlyProxy onlyOwner {
|
) external virtual onlyProxy onlyRole(SECURITY_ROLE) {
|
||||||
IdentityVerificationHubStorage storage $ = _getIdentityVerificationHubStorage();
|
IdentityVerificationHubStorage storage $ = _getIdentityVerificationHubStorage();
|
||||||
$._discloseVerifiers[attestationId] = vcAndDiscloseCircuitVerifierAddress;
|
$._discloseVerifiers[attestationId] = vcAndDiscloseCircuitVerifierAddress;
|
||||||
emit VcAndDiscloseCircuitUpdated(attestationId, vcAndDiscloseCircuitVerifierAddress);
|
emit VcAndDiscloseCircuitUpdated(attestationId, vcAndDiscloseCircuitVerifierAddress);
|
||||||
@@ -401,7 +429,7 @@ contract IdentityVerificationHubImplV2 is ImplRoot {
|
|||||||
bytes32 attestationId,
|
bytes32 attestationId,
|
||||||
uint256 typeId,
|
uint256 typeId,
|
||||||
address verifierAddress
|
address verifierAddress
|
||||||
) external virtual onlyProxy onlyOwner {
|
) external virtual onlyProxy onlyRole(SECURITY_ROLE) {
|
||||||
IdentityVerificationHubStorage storage $ = _getIdentityVerificationHubStorage();
|
IdentityVerificationHubStorage storage $ = _getIdentityVerificationHubStorage();
|
||||||
$._registerCircuitVerifiers[attestationId][typeId] = verifierAddress;
|
$._registerCircuitVerifiers[attestationId][typeId] = verifierAddress;
|
||||||
emit RegisterCircuitVerifierUpdated(typeId, verifierAddress);
|
emit RegisterCircuitVerifierUpdated(typeId, verifierAddress);
|
||||||
@@ -417,7 +445,7 @@ contract IdentityVerificationHubImplV2 is ImplRoot {
|
|||||||
bytes32 attestationId,
|
bytes32 attestationId,
|
||||||
uint256 typeId,
|
uint256 typeId,
|
||||||
address verifierAddress
|
address verifierAddress
|
||||||
) external virtual onlyProxy onlyOwner {
|
) external virtual onlyProxy onlyRole(SECURITY_ROLE) {
|
||||||
IdentityVerificationHubStorage storage $ = _getIdentityVerificationHubStorage();
|
IdentityVerificationHubStorage storage $ = _getIdentityVerificationHubStorage();
|
||||||
$._dscCircuitVerifiers[attestationId][typeId] = verifierAddress;
|
$._dscCircuitVerifiers[attestationId][typeId] = verifierAddress;
|
||||||
emit DscCircuitVerifierUpdated(typeId, verifierAddress);
|
emit DscCircuitVerifierUpdated(typeId, verifierAddress);
|
||||||
@@ -433,7 +461,7 @@ contract IdentityVerificationHubImplV2 is ImplRoot {
|
|||||||
bytes32[] calldata attestationIds,
|
bytes32[] calldata attestationIds,
|
||||||
uint256[] calldata typeIds,
|
uint256[] calldata typeIds,
|
||||||
address[] calldata verifierAddresses
|
address[] calldata verifierAddresses
|
||||||
) external virtual onlyProxy onlyOwner {
|
) external virtual onlyProxy onlyRole(SECURITY_ROLE) {
|
||||||
if (attestationIds.length != typeIds.length || attestationIds.length != verifierAddresses.length) {
|
if (attestationIds.length != typeIds.length || attestationIds.length != verifierAddresses.length) {
|
||||||
revert LengthMismatch();
|
revert LengthMismatch();
|
||||||
}
|
}
|
||||||
@@ -454,7 +482,7 @@ contract IdentityVerificationHubImplV2 is ImplRoot {
|
|||||||
bytes32[] calldata attestationIds,
|
bytes32[] calldata attestationIds,
|
||||||
uint256[] calldata typeIds,
|
uint256[] calldata typeIds,
|
||||||
address[] calldata verifierAddresses
|
address[] calldata verifierAddresses
|
||||||
) external virtual onlyProxy onlyOwner {
|
) external virtual onlyProxy onlyRole(SECURITY_ROLE) {
|
||||||
if (attestationIds.length != typeIds.length || attestationIds.length != verifierAddresses.length) {
|
if (attestationIds.length != typeIds.length || attestationIds.length != verifierAddresses.length) {
|
||||||
revert LengthMismatch();
|
revert LengthMismatch();
|
||||||
}
|
}
|
||||||
@@ -677,15 +705,11 @@ contract IdentityVerificationHubImplV2 is ImplRoot {
|
|||||||
_performUserIdentifierCheck(userContextData, vcAndDiscloseProof, header.attestationId, indices);
|
_performUserIdentifierCheck(userContextData, vcAndDiscloseProof, header.attestationId, indices);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scope 2: Root and date checks
|
// Scope 2: Root, OFAC, and current date checks
|
||||||
{
|
{
|
||||||
_performRootCheck(header.attestationId, vcAndDiscloseProof, indices);
|
_performRootCheck(header.attestationId, vcAndDiscloseProof, indices);
|
||||||
_performOfacCheck(header.attestationId, vcAndDiscloseProof, indices);
|
_performOfacCheck(header.attestationId, vcAndDiscloseProof, indices);
|
||||||
if (header.attestationId == AttestationId.AADHAAR) {
|
_performCurrentDateCheck(header.attestationId, vcAndDiscloseProof, indices);
|
||||||
_performNumericCurrentDateCheck(vcAndDiscloseProof, indices);
|
|
||||||
} else {
|
|
||||||
_performCurrentDateCheck(vcAndDiscloseProof, indices);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scope 3: Groth16 proof verification
|
// Scope 3: Groth16 proof verification
|
||||||
@@ -981,41 +1005,56 @@ contract IdentityVerificationHubImplV2 is ImplRoot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @notice Performs current date validation
|
* @notice Performs current date validation with format-aware parsing
|
||||||
|
* @dev Handles three date formats:
|
||||||
|
* - E_PASSPORT/EU_ID_CARD: 6 ASCII chars (YYMMDD)
|
||||||
|
* - SELFRICA_ID_CARD: 8 ASCII digits (YYYYMMDD)
|
||||||
|
* - AADHAAR: 3 numeric signals (year, month, day)
|
||||||
|
* @param attestationId The attestation type to determine date format
|
||||||
|
* @param vcAndDiscloseProof The proof containing date information
|
||||||
|
* @param indices Circuit-specific indices for extracting date values
|
||||||
*/
|
*/
|
||||||
function _performCurrentDateCheck(
|
function _performCurrentDateCheck(
|
||||||
|
bytes32 attestationId,
|
||||||
GenericProofStruct memory vcAndDiscloseProof,
|
GenericProofStruct memory vcAndDiscloseProof,
|
||||||
CircuitConstantsV2.DiscloseIndices memory indices
|
CircuitConstantsV2.DiscloseIndices memory indices
|
||||||
) internal view {
|
) internal view {
|
||||||
uint256[6] memory dateNum;
|
uint256 currentTimestamp;
|
||||||
for (uint256 i = 0; i < 6; i++) {
|
uint256 startIndex = indices.currentDateIndex;
|
||||||
dateNum[i] = vcAndDiscloseProof.pubSignals[indices.currentDateIndex + i];
|
|
||||||
|
if (attestationId == AttestationId.E_PASSPORT || attestationId == AttestationId.EU_ID_CARD) {
|
||||||
|
// E_PASSPORT, EU_ID_CARD: 6 ASCII chars (YYMMDD)
|
||||||
|
uint256[6] memory dateNum;
|
||||||
|
unchecked {
|
||||||
|
for (uint256 i; i < 6; ++i) {
|
||||||
|
dateNum[i] = vcAndDiscloseProof.pubSignals[startIndex + i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentTimestamp = Formatter.proofDateToUnixTimestamp(dateNum);
|
||||||
|
} else {
|
||||||
|
// AADHAAR: 3 numeric signals [year, month, day]
|
||||||
|
currentTimestamp = Formatter.proofDateToUnixTimestampNumeric(
|
||||||
|
[
|
||||||
|
vcAndDiscloseProof.pubSignals[startIndex],
|
||||||
|
vcAndDiscloseProof.pubSignals[startIndex + 1],
|
||||||
|
vcAndDiscloseProof.pubSignals[startIndex + 2]
|
||||||
|
]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
uint256 currentTimestamp = Formatter.proofDateToUnixTimestamp(dateNum);
|
_validateDateInRange(currentTimestamp);
|
||||||
uint256 startOfDay = _getStartOfDayTimestamp();
|
|
||||||
uint256 endOfDay = startOfDay + 1 days - 1;
|
|
||||||
|
|
||||||
if (currentTimestamp < startOfDay - 1 days + 1 || currentTimestamp > endOfDay + 1 days) {
|
|
||||||
revert CurrentDateNotInValidRange();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function _performNumericCurrentDateCheck(
|
/**
|
||||||
GenericProofStruct memory vcAndDiscloseProof,
|
* @notice Validates that a timestamp is within the acceptable range
|
||||||
CircuitConstantsV2.DiscloseIndices memory indices
|
* @param currentTimestamp The timestamp to validate
|
||||||
) internal view {
|
*/
|
||||||
// date is going to be 2025, 12, 13
|
function _validateDateInRange(uint256 currentTimestamp) internal view {
|
||||||
uint256[3] memory dateNum;
|
// Calculate the timestamp for the start of current date by subtracting the remainder of block.timestamp modulo 1 day
|
||||||
dateNum[0] = vcAndDiscloseProof.pubSignals[indices.currentDateIndex];
|
uint256 startOfDay = block.timestamp - (block.timestamp % 1 days);
|
||||||
dateNum[1] = vcAndDiscloseProof.pubSignals[indices.currentDateIndex + 1];
|
|
||||||
dateNum[2] = vcAndDiscloseProof.pubSignals[indices.currentDateIndex + 2];
|
|
||||||
|
|
||||||
uint256 currentTimestamp = Formatter.proofDateToUnixTimestampNumeric(dateNum);
|
// Check if timestamp is within range
|
||||||
uint256 startOfDay = _getStartOfDayTimestamp();
|
if (currentTimestamp < startOfDay - 1 days + 1 || currentTimestamp > startOfDay + 1 days - 1) {
|
||||||
uint256 endOfDay = startOfDay + 1 days - 1;
|
|
||||||
|
|
||||||
if (currentTimestamp < startOfDay - 1 days + 1 || currentTimestamp > endOfDay + 1 days) {
|
|
||||||
revert CurrentDateNotInValidRange();
|
revert CurrentDateNotInValidRange();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,3 +49,21 @@ interface IVcAndDiscloseAadhaarCircuitVerifier {
|
|||||||
uint256[19] calldata pubSignals
|
uint256[19] calldata pubSignals
|
||||||
) external view returns (bool);
|
) external view returns (bool);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IVcAndDiscloseSelfricaCircuitVerifier {
|
||||||
|
/**
|
||||||
|
* @notice Verifies a given VC and Disclose zero-knowledge proof.
|
||||||
|
* @dev This function checks the validity of the provided proof parameters.
|
||||||
|
* @param a The 'a' component of the proof.
|
||||||
|
* @param b The 'b' component of the proof.
|
||||||
|
* @param c The 'c' component of the proof.
|
||||||
|
* @param pubSignals The public signals associated with the proof.
|
||||||
|
* @return A boolean value indicating whether the proof is valid (true) or not (false).
|
||||||
|
*/
|
||||||
|
function verifyProof(
|
||||||
|
uint256[2] calldata a,
|
||||||
|
uint256[2][2] calldata b,
|
||||||
|
uint256[2] calldata c,
|
||||||
|
uint256[30] calldata pubSignals
|
||||||
|
) external view returns (bool);
|
||||||
|
}
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ abstract contract IdentityRegistryAadhaarStorageV1 is ImplRoot {
|
|||||||
* @title IdentityRegistryAadhaarImplV1
|
* @title IdentityRegistryAadhaarImplV1
|
||||||
* @notice Provides functions to register and manage identity commitments using a Merkle tree structure.
|
* @notice Provides functions to register and manage identity commitments using a Merkle tree structure.
|
||||||
* @dev Inherits from IdentityRegistryAadhaarStorageV1 and implements IIdentityRegistryAadhaarV1.
|
* @dev Inherits from IdentityRegistryAadhaarStorageV1 and implements IIdentityRegistryAadhaarV1.
|
||||||
|
*
|
||||||
|
* @custom:version 1.2.0
|
||||||
*/
|
*/
|
||||||
contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIdentityRegistryAadhaarV1 {
|
contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIdentityRegistryAadhaarV1 {
|
||||||
using InternalLeanIMT for LeanIMTData;
|
using InternalLeanIMT for LeanIMTData;
|
||||||
@@ -151,6 +153,7 @@ contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIde
|
|||||||
// ====================================================
|
// ====================================================
|
||||||
|
|
||||||
/// @notice Constructor for the IdentityRegistryAadhaarImplV1 contract.
|
/// @notice Constructor for the IdentityRegistryAadhaarImplV1 contract.
|
||||||
|
/// @custom:oz-upgrades-unsafe-allow constructor
|
||||||
constructor() {
|
constructor() {
|
||||||
_disableInitializers();
|
_disableInitializers();
|
||||||
}
|
}
|
||||||
@@ -168,6 +171,19 @@ contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIde
|
|||||||
emit RegistryInitialized(_hub);
|
emit RegistryInitialized(_hub);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Initializes AccessControl governance.
|
||||||
|
* @dev Used when upgrading from Ownable to AccessControl governance.
|
||||||
|
* This function sets up AccessControl roles on an already-initialized contract.
|
||||||
|
* It does NOT modify existing state (hub, roots, etc.).
|
||||||
|
*
|
||||||
|
* SECURITY: This function can only be called once - enforced by reinitializer(2).
|
||||||
|
* The previous version used reinitializer(1), so this upgrade uses version 2.
|
||||||
|
*/
|
||||||
|
function initializeGovernance() external reinitializer(2) {
|
||||||
|
__ImplRoot_init();
|
||||||
|
}
|
||||||
|
|
||||||
// ====================================================
|
// ====================================================
|
||||||
// External Functions - View & Checks
|
// External Functions - View & Checks
|
||||||
// ====================================================
|
// ====================================================
|
||||||
@@ -280,7 +296,7 @@ contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIde
|
|||||||
/// @notice Updates the hub address.
|
/// @notice Updates the hub address.
|
||||||
/// @dev Callable only via a proxy and restricted to the contract owner.
|
/// @dev Callable only via a proxy and restricted to the contract owner.
|
||||||
/// @param newHubAddress The new address of the hub.
|
/// @param newHubAddress The new address of the hub.
|
||||||
function updateHub(address newHubAddress) external onlyProxy onlyOwner {
|
function updateHub(address newHubAddress) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||||
if (newHubAddress == address(0)) revert HUB_ADDRESS_ZERO();
|
if (newHubAddress == address(0)) revert HUB_ADDRESS_ZERO();
|
||||||
_hub = newHubAddress;
|
_hub = newHubAddress;
|
||||||
emit HubUpdated(newHubAddress);
|
emit HubUpdated(newHubAddress);
|
||||||
@@ -289,7 +305,7 @@ contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIde
|
|||||||
/// @notice Updates the name and date of birth OFAC root.
|
/// @notice Updates the name and date of birth OFAC root.
|
||||||
/// @dev Callable only via a proxy and restricted to the contract owner.
|
/// @dev Callable only via a proxy and restricted to the contract owner.
|
||||||
/// @param newNameAndDobOfacRoot The new name and date of birth OFAC root value.
|
/// @param newNameAndDobOfacRoot The new name and date of birth OFAC root value.
|
||||||
function updateNameAndDobOfacRoot(uint256 newNameAndDobOfacRoot) external onlyProxy onlyOwner {
|
function updateNameAndDobOfacRoot(uint256 newNameAndDobOfacRoot) external onlyProxy onlyRole(OPERATIONS_ROLE) {
|
||||||
_nameAndDobOfacRoot = newNameAndDobOfacRoot;
|
_nameAndDobOfacRoot = newNameAndDobOfacRoot;
|
||||||
emit NameAndDobOfacRootUpdated(newNameAndDobOfacRoot);
|
emit NameAndDobOfacRootUpdated(newNameAndDobOfacRoot);
|
||||||
}
|
}
|
||||||
@@ -297,7 +313,7 @@ contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIde
|
|||||||
/// @notice Updates the name and year of birth OFAC root.
|
/// @notice Updates the name and year of birth OFAC root.
|
||||||
/// @dev Callable only via a proxy and restricted to the contract owner.
|
/// @dev Callable only via a proxy and restricted to the contract owner.
|
||||||
/// @param newNameAndYobOfacRoot The new name and year of birth OFAC root value.
|
/// @param newNameAndYobOfacRoot The new name and year of birth OFAC root value.
|
||||||
function updateNameAndYobOfacRoot(uint256 newNameAndYobOfacRoot) external onlyProxy onlyOwner {
|
function updateNameAndYobOfacRoot(uint256 newNameAndYobOfacRoot) external onlyProxy onlyRole(OPERATIONS_ROLE) {
|
||||||
_nameAndYobOfacRoot = newNameAndYobOfacRoot;
|
_nameAndYobOfacRoot = newNameAndYobOfacRoot;
|
||||||
emit NameAndYobOfacRootUpdated(newNameAndYobOfacRoot);
|
emit NameAndYobOfacRootUpdated(newNameAndYobOfacRoot);
|
||||||
}
|
}
|
||||||
@@ -305,7 +321,7 @@ contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIde
|
|||||||
/// @notice Registers a new UIDAI pubkey commitment.
|
/// @notice Registers a new UIDAI pubkey commitment.
|
||||||
/// @dev Callable only via a proxy and restricted to the contract owner.
|
/// @dev Callable only via a proxy and restricted to the contract owner.
|
||||||
/// @param commitment The UIDAI pubkey commitment to register.
|
/// @param commitment The UIDAI pubkey commitment to register.
|
||||||
function registerUidaiPubkeyCommitment(uint256 commitment) external onlyProxy onlyOwner {
|
function registerUidaiPubkeyCommitment(uint256 commitment) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||||
_uidaiPubkeyCommitments[commitment] = true;
|
_uidaiPubkeyCommitments[commitment] = true;
|
||||||
emit UidaiPubkeyCommitmentRegistered(commitment, block.timestamp);
|
emit UidaiPubkeyCommitmentRegistered(commitment, block.timestamp);
|
||||||
}
|
}
|
||||||
@@ -313,7 +329,7 @@ contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIde
|
|||||||
/// @notice Removes a UIDAI pubkey commitment.
|
/// @notice Removes a UIDAI pubkey commitment.
|
||||||
/// @dev Callable only via a proxy and restricted to the contract owner.
|
/// @dev Callable only via a proxy and restricted to the contract owner.
|
||||||
/// @param commitment The UIDAI pubkey commitment to remove.
|
/// @param commitment The UIDAI pubkey commitment to remove.
|
||||||
function removeUidaiPubkeyCommitment(uint256 commitment) external onlyProxy onlyOwner {
|
function removeUidaiPubkeyCommitment(uint256 commitment) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||||
delete _uidaiPubkeyCommitments[commitment];
|
delete _uidaiPubkeyCommitments[commitment];
|
||||||
emit UidaiPubkeyCommitmentRemoved(commitment, block.timestamp);
|
emit UidaiPubkeyCommitmentRemoved(commitment, block.timestamp);
|
||||||
}
|
}
|
||||||
@@ -321,7 +337,7 @@ contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIde
|
|||||||
/// @notice Updates a UIDAI pubkey commitment.
|
/// @notice Updates a UIDAI pubkey commitment.
|
||||||
/// @dev Callable only via a proxy and restricted to the contract owner.
|
/// @dev Callable only via a proxy and restricted to the contract owner.
|
||||||
/// @param commitment The UIDAI pubkey commitment to update.
|
/// @param commitment The UIDAI pubkey commitment to update.
|
||||||
function updateUidaiPubkeyCommitment(uint256 commitment) external onlyProxy onlyOwner {
|
function updateUidaiPubkeyCommitment(uint256 commitment) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||||
_uidaiPubkeyCommitments[commitment] = true;
|
_uidaiPubkeyCommitments[commitment] = true;
|
||||||
emit UidaiPubkeyCommitmentUpdated(commitment, block.timestamp);
|
emit UidaiPubkeyCommitmentUpdated(commitment, block.timestamp);
|
||||||
}
|
}
|
||||||
@@ -335,7 +351,7 @@ contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIde
|
|||||||
bytes32 attestationId,
|
bytes32 attestationId,
|
||||||
uint256 nullifier,
|
uint256 nullifier,
|
||||||
uint256 commitment
|
uint256 commitment
|
||||||
) external onlyProxy onlyOwner {
|
) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||||
_nullifiers[nullifier] = true;
|
_nullifiers[nullifier] = true;
|
||||||
uint256 imt_root = _identityCommitmentIMT._insert(commitment);
|
uint256 imt_root = _identityCommitmentIMT._insert(commitment);
|
||||||
_rootTimestamps[imt_root] = block.timestamp;
|
_rootTimestamps[imt_root] = block.timestamp;
|
||||||
@@ -352,7 +368,7 @@ contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIde
|
|||||||
uint256 oldLeaf,
|
uint256 oldLeaf,
|
||||||
uint256 newLeaf,
|
uint256 newLeaf,
|
||||||
uint256[] calldata siblingNodes
|
uint256[] calldata siblingNodes
|
||||||
) external onlyProxy onlyOwner {
|
) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||||
uint256 imt_root = _identityCommitmentIMT._update(oldLeaf, newLeaf, siblingNodes);
|
uint256 imt_root = _identityCommitmentIMT._update(oldLeaf, newLeaf, siblingNodes);
|
||||||
_rootTimestamps[imt_root] = block.timestamp;
|
_rootTimestamps[imt_root] = block.timestamp;
|
||||||
emit DevCommitmentUpdated(oldLeaf, newLeaf, imt_root, block.timestamp);
|
emit DevCommitmentUpdated(oldLeaf, newLeaf, imt_root, block.timestamp);
|
||||||
@@ -362,7 +378,10 @@ contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIde
|
|||||||
/// @dev Caller must be the owner. Provides sibling nodes for proof of position.
|
/// @dev Caller must be the owner. Provides sibling nodes for proof of position.
|
||||||
/// @param oldLeaf The identity commitment to remove.
|
/// @param oldLeaf The identity commitment to remove.
|
||||||
/// @param siblingNodes An array of sibling nodes for Merkle proof generation.
|
/// @param siblingNodes An array of sibling nodes for Merkle proof generation.
|
||||||
function devRemoveCommitment(uint256 oldLeaf, uint256[] calldata siblingNodes) external onlyProxy onlyOwner {
|
function devRemoveCommitment(
|
||||||
|
uint256 oldLeaf,
|
||||||
|
uint256[] calldata siblingNodes
|
||||||
|
) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||||
uint256 imt_root = _identityCommitmentIMT._remove(oldLeaf, siblingNodes);
|
uint256 imt_root = _identityCommitmentIMT._remove(oldLeaf, siblingNodes);
|
||||||
_rootTimestamps[imt_root] = block.timestamp;
|
_rootTimestamps[imt_root] = block.timestamp;
|
||||||
emit DevCommitmentRemoved(oldLeaf, imt_root, block.timestamp);
|
emit DevCommitmentRemoved(oldLeaf, imt_root, block.timestamp);
|
||||||
|
|||||||
@@ -77,6 +77,8 @@ abstract contract IdentityRegistryIdCardStorageV1 is ImplRoot {
|
|||||||
* @title IdentityRegistryImplV1
|
* @title IdentityRegistryImplV1
|
||||||
* @notice Provides functions to register and manage identity commitments using a Merkle tree structure.
|
* @notice Provides functions to register and manage identity commitments using a Merkle tree structure.
|
||||||
* @dev Inherits from IdentityRegistryStorageV1 and implements IIdentityRegistryV1.
|
* @dev Inherits from IdentityRegistryStorageV1 and implements IIdentityRegistryV1.
|
||||||
|
*
|
||||||
|
* @custom:version 1.2.0
|
||||||
*/
|
*/
|
||||||
contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdentityRegistryIdCardV1 {
|
contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdentityRegistryIdCardV1 {
|
||||||
using InternalLeanIMT for LeanIMTData;
|
using InternalLeanIMT for LeanIMTData;
|
||||||
@@ -162,6 +164,7 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
|
|||||||
/**
|
/**
|
||||||
* @notice Constructor that disables initializers.
|
* @notice Constructor that disables initializers.
|
||||||
* @dev Prevents direct initialization of the implementation contract.
|
* @dev Prevents direct initialization of the implementation contract.
|
||||||
|
* @custom:oz-upgrades-unsafe-allow constructor
|
||||||
*/
|
*/
|
||||||
constructor() {
|
constructor() {
|
||||||
_disableInitializers();
|
_disableInitializers();
|
||||||
@@ -181,6 +184,19 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
|
|||||||
emit RegistryInitialized(_hub);
|
emit RegistryInitialized(_hub);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Initializes governance for upgraded contracts.
|
||||||
|
* @dev Used when upgrading from Ownable to AccessControl governance.
|
||||||
|
* This function sets up AccessControl roles on an already-initialized contract.
|
||||||
|
* It does NOT modify existing state (hub, roots, etc.).
|
||||||
|
*
|
||||||
|
* SECURITY: This function can only be called once - enforced by reinitializer(2).
|
||||||
|
* The previous version used reinitializer(1), so this upgrade uses version 2.
|
||||||
|
*/
|
||||||
|
function initializeGovernance() external reinitializer(2) {
|
||||||
|
__ImplRoot_init();
|
||||||
|
}
|
||||||
|
|
||||||
// ====================================================
|
// ====================================================
|
||||||
// External Functions - View & Checks
|
// External Functions - View & Checks
|
||||||
// ====================================================
|
// ====================================================
|
||||||
@@ -380,7 +396,7 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
|
|||||||
* @dev Callable only via a proxy and restricted to the contract owner.
|
* @dev Callable only via a proxy and restricted to the contract owner.
|
||||||
* @param newHubAddress The new address of the hub.
|
* @param newHubAddress The new address of the hub.
|
||||||
*/
|
*/
|
||||||
function updateHub(address newHubAddress) external onlyProxy onlyOwner {
|
function updateHub(address newHubAddress) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||||
_hub = newHubAddress;
|
_hub = newHubAddress;
|
||||||
emit HubUpdated(newHubAddress);
|
emit HubUpdated(newHubAddress);
|
||||||
}
|
}
|
||||||
@@ -390,7 +406,7 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
|
|||||||
* @dev Callable only via a proxy and restricted to the contract owner.
|
* @dev Callable only via a proxy and restricted to the contract owner.
|
||||||
* @param newNameAndDobOfacRoot The new name and date of birth OFAC root value.
|
* @param newNameAndDobOfacRoot The new name and date of birth OFAC root value.
|
||||||
*/
|
*/
|
||||||
function updateNameAndDobOfacRoot(uint256 newNameAndDobOfacRoot) external onlyProxy onlyOwner {
|
function updateNameAndDobOfacRoot(uint256 newNameAndDobOfacRoot) external onlyProxy onlyRole(OPERATIONS_ROLE) {
|
||||||
_nameAndDobOfacRoot = newNameAndDobOfacRoot;
|
_nameAndDobOfacRoot = newNameAndDobOfacRoot;
|
||||||
emit NameAndDobOfacRootUpdated(newNameAndDobOfacRoot);
|
emit NameAndDobOfacRootUpdated(newNameAndDobOfacRoot);
|
||||||
}
|
}
|
||||||
@@ -400,7 +416,7 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
|
|||||||
* @dev Callable only via a proxy and restricted to the contract owner.
|
* @dev Callable only via a proxy and restricted to the contract owner.
|
||||||
* @param newNameAndYobOfacRoot The new name and year of birth OFAC root value.
|
* @param newNameAndYobOfacRoot The new name and year of birth OFAC root value.
|
||||||
*/
|
*/
|
||||||
function updateNameAndYobOfacRoot(uint256 newNameAndYobOfacRoot) external onlyProxy onlyOwner {
|
function updateNameAndYobOfacRoot(uint256 newNameAndYobOfacRoot) external onlyProxy onlyRole(OPERATIONS_ROLE) {
|
||||||
_nameAndYobOfacRoot = newNameAndYobOfacRoot;
|
_nameAndYobOfacRoot = newNameAndYobOfacRoot;
|
||||||
emit NameAndYobOfacRootUpdated(newNameAndYobOfacRoot);
|
emit NameAndYobOfacRootUpdated(newNameAndYobOfacRoot);
|
||||||
}
|
}
|
||||||
@@ -410,7 +426,7 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
|
|||||||
* @dev Callable only via a proxy and restricted to the contract owner.
|
* @dev Callable only via a proxy and restricted to the contract owner.
|
||||||
* @param newCscaRoot The new CSCA root value.
|
* @param newCscaRoot The new CSCA root value.
|
||||||
*/
|
*/
|
||||||
function updateCscaRoot(uint256 newCscaRoot) external onlyProxy onlyOwner {
|
function updateCscaRoot(uint256 newCscaRoot) external onlyProxy onlyRole(OPERATIONS_ROLE) {
|
||||||
_cscaRoot = newCscaRoot;
|
_cscaRoot = newCscaRoot;
|
||||||
emit CscaRootUpdated(newCscaRoot);
|
emit CscaRootUpdated(newCscaRoot);
|
||||||
}
|
}
|
||||||
@@ -426,7 +442,7 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
|
|||||||
bytes32 attestationId,
|
bytes32 attestationId,
|
||||||
uint256 nullifier,
|
uint256 nullifier,
|
||||||
uint256 commitment
|
uint256 commitment
|
||||||
) external onlyProxy onlyOwner {
|
) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||||
_nullifiers[attestationId][nullifier] = true;
|
_nullifiers[attestationId][nullifier] = true;
|
||||||
uint256 imt_root = _addCommitment(_identityCommitmentIMT, commitment);
|
uint256 imt_root = _addCommitment(_identityCommitmentIMT, commitment);
|
||||||
_rootTimestamps[imt_root] = block.timestamp;
|
_rootTimestamps[imt_root] = block.timestamp;
|
||||||
@@ -445,7 +461,7 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
|
|||||||
uint256 oldLeaf,
|
uint256 oldLeaf,
|
||||||
uint256 newLeaf,
|
uint256 newLeaf,
|
||||||
uint256[] calldata siblingNodes
|
uint256[] calldata siblingNodes
|
||||||
) external onlyProxy onlyOwner {
|
) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||||
uint256 imt_root = _updateCommitment(_identityCommitmentIMT, oldLeaf, newLeaf, siblingNodes);
|
uint256 imt_root = _updateCommitment(_identityCommitmentIMT, oldLeaf, newLeaf, siblingNodes);
|
||||||
_rootTimestamps[imt_root] = block.timestamp;
|
_rootTimestamps[imt_root] = block.timestamp;
|
||||||
emit DevCommitmentUpdated(oldLeaf, newLeaf, imt_root, block.timestamp);
|
emit DevCommitmentUpdated(oldLeaf, newLeaf, imt_root, block.timestamp);
|
||||||
@@ -457,7 +473,10 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
|
|||||||
* @param oldLeaf The identity commitment to remove.
|
* @param oldLeaf The identity commitment to remove.
|
||||||
* @param siblingNodes An array of sibling nodes for Merkle proof generation.
|
* @param siblingNodes An array of sibling nodes for Merkle proof generation.
|
||||||
*/
|
*/
|
||||||
function devRemoveCommitment(uint256 oldLeaf, uint256[] calldata siblingNodes) external onlyProxy onlyOwner {
|
function devRemoveCommitment(
|
||||||
|
uint256 oldLeaf,
|
||||||
|
uint256[] calldata siblingNodes
|
||||||
|
) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||||
uint256 imt_root = _removeCommitment(_identityCommitmentIMT, oldLeaf, siblingNodes);
|
uint256 imt_root = _removeCommitment(_identityCommitmentIMT, oldLeaf, siblingNodes);
|
||||||
_rootTimestamps[imt_root] = block.timestamp;
|
_rootTimestamps[imt_root] = block.timestamp;
|
||||||
emit DevCommitmentRemoved(oldLeaf, imt_root, block.timestamp);
|
emit DevCommitmentRemoved(oldLeaf, imt_root, block.timestamp);
|
||||||
@@ -468,7 +487,7 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
|
|||||||
* @dev Callable only by the owner for testing or administration.
|
* @dev Callable only by the owner for testing or administration.
|
||||||
* @param dscCommitment The DSC key commitment to add.
|
* @param dscCommitment The DSC key commitment to add.
|
||||||
*/
|
*/
|
||||||
function devAddDscKeyCommitment(uint256 dscCommitment) external onlyProxy onlyOwner {
|
function devAddDscKeyCommitment(uint256 dscCommitment) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||||
_isRegisteredDscKeyCommitment[dscCommitment] = true;
|
_isRegisteredDscKeyCommitment[dscCommitment] = true;
|
||||||
uint256 imt_root = _addCommitment(_dscKeyCommitmentIMT, dscCommitment);
|
uint256 imt_root = _addCommitment(_dscKeyCommitmentIMT, dscCommitment);
|
||||||
uint256 index = _dscKeyCommitmentIMT._indexOf(dscCommitment);
|
uint256 index = _dscKeyCommitmentIMT._indexOf(dscCommitment);
|
||||||
@@ -486,7 +505,7 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
|
|||||||
uint256 oldLeaf,
|
uint256 oldLeaf,
|
||||||
uint256 newLeaf,
|
uint256 newLeaf,
|
||||||
uint256[] calldata siblingNodes
|
uint256[] calldata siblingNodes
|
||||||
) external onlyProxy onlyOwner {
|
) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||||
uint256 imt_root = _updateCommitment(_dscKeyCommitmentIMT, oldLeaf, newLeaf, siblingNodes);
|
uint256 imt_root = _updateCommitment(_dscKeyCommitmentIMT, oldLeaf, newLeaf, siblingNodes);
|
||||||
emit DevDscKeyCommitmentUpdated(oldLeaf, newLeaf, imt_root);
|
emit DevDscKeyCommitmentUpdated(oldLeaf, newLeaf, imt_root);
|
||||||
}
|
}
|
||||||
@@ -497,7 +516,10 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
|
|||||||
* @param oldLeaf The DSC key commitment to remove.
|
* @param oldLeaf The DSC key commitment to remove.
|
||||||
* @param siblingNodes An array of sibling nodes for Merkle proof generation.
|
* @param siblingNodes An array of sibling nodes for Merkle proof generation.
|
||||||
*/
|
*/
|
||||||
function devRemoveDscKeyCommitment(uint256 oldLeaf, uint256[] calldata siblingNodes) external onlyProxy onlyOwner {
|
function devRemoveDscKeyCommitment(
|
||||||
|
uint256 oldLeaf,
|
||||||
|
uint256[] calldata siblingNodes
|
||||||
|
) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||||
uint256 imt_root = _removeCommitment(_dscKeyCommitmentIMT, oldLeaf, siblingNodes);
|
uint256 imt_root = _removeCommitment(_dscKeyCommitmentIMT, oldLeaf, siblingNodes);
|
||||||
emit DevDscKeyCommitmentRemoved(oldLeaf, imt_root);
|
emit DevDscKeyCommitmentRemoved(oldLeaf, imt_root);
|
||||||
}
|
}
|
||||||
@@ -513,7 +535,7 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
|
|||||||
bytes32 attestationId,
|
bytes32 attestationId,
|
||||||
uint256 nullifier,
|
uint256 nullifier,
|
||||||
bool state
|
bool state
|
||||||
) external onlyProxy onlyOwner {
|
) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||||
_nullifiers[attestationId][nullifier] = state;
|
_nullifiers[attestationId][nullifier] = state;
|
||||||
emit DevNullifierStateChanged(attestationId, nullifier, state);
|
emit DevNullifierStateChanged(attestationId, nullifier, state);
|
||||||
}
|
}
|
||||||
@@ -524,7 +546,10 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
|
|||||||
* @param dscCommitment The DSC key commitment.
|
* @param dscCommitment The DSC key commitment.
|
||||||
* @param state The new state of the DSC key commitment (true for registered, false for not registered).
|
* @param state The new state of the DSC key commitment (true for registered, false for not registered).
|
||||||
*/
|
*/
|
||||||
function devChangeDscKeyCommitmentState(uint256 dscCommitment, bool state) external onlyProxy onlyOwner {
|
function devChangeDscKeyCommitmentState(
|
||||||
|
uint256 dscCommitment,
|
||||||
|
bool state
|
||||||
|
) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||||
_isRegisteredDscKeyCommitment[dscCommitment] = state;
|
_isRegisteredDscKeyCommitment[dscCommitment] = state;
|
||||||
emit DevDscKeyCommitmentStateChanged(dscCommitment, state);
|
emit DevDscKeyCommitmentStateChanged(dscCommitment, state);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,8 @@ abstract contract IdentityRegistryStorageV1 is ImplRoot {
|
|||||||
* @title IdentityRegistryImplV1
|
* @title IdentityRegistryImplV1
|
||||||
* @notice Provides functions to register and manage identity commitments using a Merkle tree structure.
|
* @notice Provides functions to register and manage identity commitments using a Merkle tree structure.
|
||||||
* @dev Inherits from IdentityRegistryStorageV1 and implements IIdentityRegistryV1.
|
* @dev Inherits from IdentityRegistryStorageV1 and implements IIdentityRegistryV1.
|
||||||
|
*
|
||||||
|
* @custom:version 1.2.0
|
||||||
*/
|
*/
|
||||||
contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV1 {
|
contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV1 {
|
||||||
using InternalLeanIMT for LeanIMTData;
|
using InternalLeanIMT for LeanIMTData;
|
||||||
@@ -169,6 +171,7 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
|
|||||||
/**
|
/**
|
||||||
* @notice Constructor that disables initializers.
|
* @notice Constructor that disables initializers.
|
||||||
* @dev Prevents direct initialization of the implementation contract.
|
* @dev Prevents direct initialization of the implementation contract.
|
||||||
|
* @custom:oz-upgrades-unsafe-allow constructor
|
||||||
*/
|
*/
|
||||||
constructor() {
|
constructor() {
|
||||||
_disableInitializers();
|
_disableInitializers();
|
||||||
@@ -188,6 +191,19 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
|
|||||||
emit RegistryInitialized(hubAddress);
|
emit RegistryInitialized(hubAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Initializes governance for upgraded contracts.
|
||||||
|
* @dev Used when upgrading from Ownable to AccessControl governance.
|
||||||
|
* This function sets up AccessControl roles on an already-initialized contract.
|
||||||
|
* It does NOT modify existing state (hub, roots, etc.).
|
||||||
|
*
|
||||||
|
* SECURITY: This function can only be called once - enforced by reinitializer(2).
|
||||||
|
* The previous version used reinitializer(1), so this upgrade uses version 2.
|
||||||
|
*/
|
||||||
|
function initializeGovernance() external reinitializer(2) {
|
||||||
|
__ImplRoot_init();
|
||||||
|
}
|
||||||
|
|
||||||
// ====================================================
|
// ====================================================
|
||||||
// External Functions - View & Checks
|
// External Functions - View & Checks
|
||||||
// ====================================================
|
// ====================================================
|
||||||
@@ -403,7 +419,7 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
|
|||||||
* @dev Callable only via a proxy and restricted to the contract owner.
|
* @dev Callable only via a proxy and restricted to the contract owner.
|
||||||
* @param newHubAddress The new address of the hub.
|
* @param newHubAddress The new address of the hub.
|
||||||
*/
|
*/
|
||||||
function updateHub(address newHubAddress) external onlyProxy onlyOwner {
|
function updateHub(address newHubAddress) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||||
_hub = newHubAddress;
|
_hub = newHubAddress;
|
||||||
emit HubUpdated(newHubAddress);
|
emit HubUpdated(newHubAddress);
|
||||||
}
|
}
|
||||||
@@ -413,7 +429,7 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
|
|||||||
* @dev Callable only via a proxy and restricted to the contract owner.
|
* @dev Callable only via a proxy and restricted to the contract owner.
|
||||||
* @param newPassportNoOfacRoot The new passport number OFAC root value.
|
* @param newPassportNoOfacRoot The new passport number OFAC root value.
|
||||||
*/
|
*/
|
||||||
function updatePassportNoOfacRoot(uint256 newPassportNoOfacRoot) external onlyProxy onlyOwner {
|
function updatePassportNoOfacRoot(uint256 newPassportNoOfacRoot) external onlyProxy onlyRole(OPERATIONS_ROLE) {
|
||||||
_passportNoOfacRoot = newPassportNoOfacRoot;
|
_passportNoOfacRoot = newPassportNoOfacRoot;
|
||||||
emit PassportNoOfacRootUpdated(newPassportNoOfacRoot);
|
emit PassportNoOfacRootUpdated(newPassportNoOfacRoot);
|
||||||
}
|
}
|
||||||
@@ -423,7 +439,7 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
|
|||||||
* @dev Callable only via a proxy and restricted to the contract owner.
|
* @dev Callable only via a proxy and restricted to the contract owner.
|
||||||
* @param newNameAndDobOfacRoot The new name and date of birth OFAC root value.
|
* @param newNameAndDobOfacRoot The new name and date of birth OFAC root value.
|
||||||
*/
|
*/
|
||||||
function updateNameAndDobOfacRoot(uint256 newNameAndDobOfacRoot) external onlyProxy onlyOwner {
|
function updateNameAndDobOfacRoot(uint256 newNameAndDobOfacRoot) external onlyProxy onlyRole(OPERATIONS_ROLE) {
|
||||||
_nameAndDobOfacRoot = newNameAndDobOfacRoot;
|
_nameAndDobOfacRoot = newNameAndDobOfacRoot;
|
||||||
emit NameAndDobOfacRootUpdated(newNameAndDobOfacRoot);
|
emit NameAndDobOfacRootUpdated(newNameAndDobOfacRoot);
|
||||||
}
|
}
|
||||||
@@ -433,7 +449,7 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
|
|||||||
* @dev Callable only via a proxy and restricted to the contract owner.
|
* @dev Callable only via a proxy and restricted to the contract owner.
|
||||||
* @param newNameAndYobOfacRoot The new name and year of birth OFAC root value.
|
* @param newNameAndYobOfacRoot The new name and year of birth OFAC root value.
|
||||||
*/
|
*/
|
||||||
function updateNameAndYobOfacRoot(uint256 newNameAndYobOfacRoot) external onlyProxy onlyOwner {
|
function updateNameAndYobOfacRoot(uint256 newNameAndYobOfacRoot) external onlyProxy onlyRole(OPERATIONS_ROLE) {
|
||||||
_nameAndYobOfacRoot = newNameAndYobOfacRoot;
|
_nameAndYobOfacRoot = newNameAndYobOfacRoot;
|
||||||
emit NameAndYobOfacRootUpdated(newNameAndYobOfacRoot);
|
emit NameAndYobOfacRootUpdated(newNameAndYobOfacRoot);
|
||||||
}
|
}
|
||||||
@@ -443,7 +459,7 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
|
|||||||
* @dev Callable only via a proxy and restricted to the contract owner.
|
* @dev Callable only via a proxy and restricted to the contract owner.
|
||||||
* @param newCscaRoot The new CSCA root value.
|
* @param newCscaRoot The new CSCA root value.
|
||||||
*/
|
*/
|
||||||
function updateCscaRoot(uint256 newCscaRoot) external onlyProxy onlyOwner {
|
function updateCscaRoot(uint256 newCscaRoot) external onlyProxy onlyRole(OPERATIONS_ROLE) {
|
||||||
_cscaRoot = newCscaRoot;
|
_cscaRoot = newCscaRoot;
|
||||||
emit CscaRootUpdated(newCscaRoot);
|
emit CscaRootUpdated(newCscaRoot);
|
||||||
}
|
}
|
||||||
@@ -459,7 +475,7 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
|
|||||||
bytes32 attestationId,
|
bytes32 attestationId,
|
||||||
uint256 nullifier,
|
uint256 nullifier,
|
||||||
uint256 commitment
|
uint256 commitment
|
||||||
) external onlyProxy onlyOwner {
|
) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||||
_nullifiers[attestationId][nullifier] = true;
|
_nullifiers[attestationId][nullifier] = true;
|
||||||
uint256 imt_root = _addCommitment(_identityCommitmentIMT, commitment);
|
uint256 imt_root = _addCommitment(_identityCommitmentIMT, commitment);
|
||||||
_rootTimestamps[imt_root] = block.timestamp;
|
_rootTimestamps[imt_root] = block.timestamp;
|
||||||
@@ -478,7 +494,7 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
|
|||||||
uint256 oldLeaf,
|
uint256 oldLeaf,
|
||||||
uint256 newLeaf,
|
uint256 newLeaf,
|
||||||
uint256[] calldata siblingNodes
|
uint256[] calldata siblingNodes
|
||||||
) external onlyProxy onlyOwner {
|
) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||||
uint256 imt_root = _updateCommitment(_identityCommitmentIMT, oldLeaf, newLeaf, siblingNodes);
|
uint256 imt_root = _updateCommitment(_identityCommitmentIMT, oldLeaf, newLeaf, siblingNodes);
|
||||||
_rootTimestamps[imt_root] = block.timestamp;
|
_rootTimestamps[imt_root] = block.timestamp;
|
||||||
emit DevCommitmentUpdated(oldLeaf, newLeaf, imt_root, block.timestamp);
|
emit DevCommitmentUpdated(oldLeaf, newLeaf, imt_root, block.timestamp);
|
||||||
@@ -490,7 +506,10 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
|
|||||||
* @param oldLeaf The identity commitment to remove.
|
* @param oldLeaf The identity commitment to remove.
|
||||||
* @param siblingNodes An array of sibling nodes for Merkle proof generation.
|
* @param siblingNodes An array of sibling nodes for Merkle proof generation.
|
||||||
*/
|
*/
|
||||||
function devRemoveCommitment(uint256 oldLeaf, uint256[] calldata siblingNodes) external onlyProxy onlyOwner {
|
function devRemoveCommitment(
|
||||||
|
uint256 oldLeaf,
|
||||||
|
uint256[] calldata siblingNodes
|
||||||
|
) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||||
uint256 imt_root = _removeCommitment(_identityCommitmentIMT, oldLeaf, siblingNodes);
|
uint256 imt_root = _removeCommitment(_identityCommitmentIMT, oldLeaf, siblingNodes);
|
||||||
_rootTimestamps[imt_root] = block.timestamp;
|
_rootTimestamps[imt_root] = block.timestamp;
|
||||||
emit DevCommitmentRemoved(oldLeaf, imt_root, block.timestamp);
|
emit DevCommitmentRemoved(oldLeaf, imt_root, block.timestamp);
|
||||||
@@ -501,7 +520,7 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
|
|||||||
* @dev Callable only by the owner for testing or administration.
|
* @dev Callable only by the owner for testing or administration.
|
||||||
* @param dscCommitment The DSC key commitment to add.
|
* @param dscCommitment The DSC key commitment to add.
|
||||||
*/
|
*/
|
||||||
function devAddDscKeyCommitment(uint256 dscCommitment) external onlyProxy onlyOwner {
|
function devAddDscKeyCommitment(uint256 dscCommitment) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||||
_isRegisteredDscKeyCommitment[dscCommitment] = true;
|
_isRegisteredDscKeyCommitment[dscCommitment] = true;
|
||||||
uint256 imt_root = _addCommitment(_dscKeyCommitmentIMT, dscCommitment);
|
uint256 imt_root = _addCommitment(_dscKeyCommitmentIMT, dscCommitment);
|
||||||
uint256 index = _dscKeyCommitmentIMT._indexOf(dscCommitment);
|
uint256 index = _dscKeyCommitmentIMT._indexOf(dscCommitment);
|
||||||
@@ -519,7 +538,7 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
|
|||||||
uint256 oldLeaf,
|
uint256 oldLeaf,
|
||||||
uint256 newLeaf,
|
uint256 newLeaf,
|
||||||
uint256[] calldata siblingNodes
|
uint256[] calldata siblingNodes
|
||||||
) external onlyProxy onlyOwner {
|
) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||||
uint256 imt_root = _updateCommitment(_dscKeyCommitmentIMT, oldLeaf, newLeaf, siblingNodes);
|
uint256 imt_root = _updateCommitment(_dscKeyCommitmentIMT, oldLeaf, newLeaf, siblingNodes);
|
||||||
emit DevDscKeyCommitmentUpdated(oldLeaf, newLeaf, imt_root);
|
emit DevDscKeyCommitmentUpdated(oldLeaf, newLeaf, imt_root);
|
||||||
}
|
}
|
||||||
@@ -530,7 +549,10 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
|
|||||||
* @param oldLeaf The DSC key commitment to remove.
|
* @param oldLeaf The DSC key commitment to remove.
|
||||||
* @param siblingNodes An array of sibling nodes for Merkle proof generation.
|
* @param siblingNodes An array of sibling nodes for Merkle proof generation.
|
||||||
*/
|
*/
|
||||||
function devRemoveDscKeyCommitment(uint256 oldLeaf, uint256[] calldata siblingNodes) external onlyProxy onlyOwner {
|
function devRemoveDscKeyCommitment(
|
||||||
|
uint256 oldLeaf,
|
||||||
|
uint256[] calldata siblingNodes
|
||||||
|
) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||||
uint256 imt_root = _removeCommitment(_dscKeyCommitmentIMT, oldLeaf, siblingNodes);
|
uint256 imt_root = _removeCommitment(_dscKeyCommitmentIMT, oldLeaf, siblingNodes);
|
||||||
emit DevDscKeyCommitmentRemoved(oldLeaf, imt_root);
|
emit DevDscKeyCommitmentRemoved(oldLeaf, imt_root);
|
||||||
}
|
}
|
||||||
@@ -546,7 +568,7 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
|
|||||||
bytes32 attestationId,
|
bytes32 attestationId,
|
||||||
uint256 nullifier,
|
uint256 nullifier,
|
||||||
bool state
|
bool state
|
||||||
) external onlyProxy onlyOwner {
|
) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||||
_nullifiers[attestationId][nullifier] = state;
|
_nullifiers[attestationId][nullifier] = state;
|
||||||
emit DevNullifierStateChanged(attestationId, nullifier, state);
|
emit DevNullifierStateChanged(attestationId, nullifier, state);
|
||||||
}
|
}
|
||||||
@@ -557,7 +579,10 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
|
|||||||
* @param dscCommitment The DSC key commitment.
|
* @param dscCommitment The DSC key commitment.
|
||||||
* @param state The new state of the DSC key commitment (true for registered, false for not registered).
|
* @param state The new state of the DSC key commitment (true for registered, false for not registered).
|
||||||
*/
|
*/
|
||||||
function devChangeDscKeyCommitmentState(uint256 dscCommitment, bool state) external onlyProxy onlyOwner {
|
function devChangeDscKeyCommitmentState(
|
||||||
|
uint256 dscCommitment,
|
||||||
|
bool state
|
||||||
|
) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||||
_isRegisteredDscKeyCommitment[dscCommitment] = state;
|
_isRegisteredDscKeyCommitment[dscCommitment] = state;
|
||||||
emit DevDscKeyCommitmentStateChanged(dscCommitment, state);
|
emit DevDscKeyCommitmentStateChanged(dscCommitment, state);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,22 +3,36 @@ pragma solidity 0.8.28;
|
|||||||
|
|
||||||
import {IIdentityVerificationHubV1} from "../interfaces/IIdentityVerificationHubV1.sol";
|
import {IIdentityVerificationHubV1} from "../interfaces/IIdentityVerificationHubV1.sol";
|
||||||
import {IIdentityRegistryV1} from "../interfaces/IIdentityRegistryV1.sol";
|
import {IIdentityRegistryV1} from "../interfaces/IIdentityRegistryV1.sol";
|
||||||
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
|
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
|
||||||
import {CircuitConstants} from "../constants/CircuitConstants.sol";
|
import {CircuitConstants} from "../constants/CircuitConstants.sol";
|
||||||
|
|
||||||
/// @title VerifyAll
|
/// @title VerifyAll
|
||||||
/// @notice A contract for verifying identity proofs and revealing selected data
|
/// @notice A contract for verifying identity proofs and revealing selected data
|
||||||
/// @dev This contract interacts with IdentityVerificationHub and IdentityRegistry
|
/// @dev This contract interacts with IdentityVerificationHub and IdentityRegistry
|
||||||
contract VerifyAll is Ownable {
|
contract VerifyAll is AccessControl {
|
||||||
|
/// @notice Critical operations and role management requiring 3/5 multisig consensus
|
||||||
|
bytes32 public constant SECURITY_ROLE = keccak256("SECURITY_ROLE");
|
||||||
|
|
||||||
|
/// @notice Standard operations requiring 2/5 multisig consensus
|
||||||
|
bytes32 public constant OPERATIONS_ROLE = keccak256("OPERATIONS_ROLE");
|
||||||
|
|
||||||
IIdentityVerificationHubV1 public hub;
|
IIdentityVerificationHubV1 public hub;
|
||||||
IIdentityRegistryV1 public registry;
|
IIdentityRegistryV1 public registry;
|
||||||
|
|
||||||
/// @notice Initializes the contract with hub and registry addresses
|
/// @notice Initializes the contract with hub and registry addresses
|
||||||
/// @param hubAddress The address of the IdentityVerificationHub contract
|
/// @param hubAddress The address of the IdentityVerificationHub contract
|
||||||
/// @param registryAddress The address of the IdentityRegistry contract
|
/// @param registryAddress The address of the IdentityRegistry contract
|
||||||
constructor(address hubAddress, address registryAddress) Ownable(msg.sender) {
|
constructor(address hubAddress, address registryAddress) {
|
||||||
hub = IIdentityVerificationHubV1(hubAddress);
|
hub = IIdentityVerificationHubV1(hubAddress);
|
||||||
registry = IIdentityRegistryV1(registryAddress);
|
registry = IIdentityRegistryV1(registryAddress);
|
||||||
|
|
||||||
|
// Grant all roles to deployer initially
|
||||||
|
_grantRole(SECURITY_ROLE, msg.sender);
|
||||||
|
_grantRole(OPERATIONS_ROLE, msg.sender);
|
||||||
|
|
||||||
|
// Set role admins - SECURITY_ROLE manages all roles
|
||||||
|
_setRoleAdmin(SECURITY_ROLE, SECURITY_ROLE);
|
||||||
|
_setRoleAdmin(OPERATIONS_ROLE, SECURITY_ROLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice Verifies identity proof and reveals selected data
|
/// @notice Verifies identity proof and reveals selected data
|
||||||
@@ -107,15 +121,15 @@ contract VerifyAll is Ownable {
|
|||||||
|
|
||||||
/// @notice Updates the hub contract address
|
/// @notice Updates the hub contract address
|
||||||
/// @param hubAddress The new hub contract address
|
/// @param hubAddress The new hub contract address
|
||||||
/// @dev Only callable by the contract owner
|
/// @dev Only callable by accounts with SECURITY_ROLE
|
||||||
function setHub(address hubAddress) external onlyOwner {
|
function setHub(address hubAddress) external onlyRole(SECURITY_ROLE) {
|
||||||
hub = IIdentityVerificationHubV1(hubAddress);
|
hub = IIdentityVerificationHubV1(hubAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice Updates the registry contract address
|
/// @notice Updates the registry contract address
|
||||||
/// @param registryAddress The new registry contract address
|
/// @param registryAddress The new registry contract address
|
||||||
/// @dev Only callable by the contract owner
|
/// @dev Only callable by accounts with SECURITY_ROLE
|
||||||
function setRegistry(address registryAddress) external onlyOwner {
|
function setRegistry(address registryAddress) external onlyRole(SECURITY_ROLE) {
|
||||||
registry = IIdentityRegistryV1(registryAddress);
|
registry = IIdentityRegistryV1(registryAddress);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
70
contracts/contracts/tests/MockOwnableHub.sol
Normal file
70
contracts/contracts/tests/MockOwnableHub.sol
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
pragma solidity 0.8.28;
|
||||||
|
|
||||||
|
import {MockOwnableImplRoot} from "./MockOwnableImplRoot.sol";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @title MockOwnableHub
|
||||||
|
* @dev Mock contract that simulates the OLD production Hub using Ownable
|
||||||
|
* This represents what's currently deployed in production before the governance upgrade.
|
||||||
|
*/
|
||||||
|
contract MockOwnableHub is MockOwnableImplRoot {
|
||||||
|
/// @notice Circuit version for compatibility
|
||||||
|
uint256 private _circuitVersion;
|
||||||
|
|
||||||
|
/// @notice Registry address
|
||||||
|
address private _registry;
|
||||||
|
|
||||||
|
/// @notice Event emitted when hub is initialized
|
||||||
|
event HubInitialized();
|
||||||
|
|
||||||
|
/// @notice Event emitted when registry is updated
|
||||||
|
event RegistryUpdated(address indexed registry);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Constructor that disables initializers for the implementation contract.
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
_disableInitializers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Initializes the Hub contract (simulates production initialization)
|
||||||
|
*/
|
||||||
|
function initialize() external initializer {
|
||||||
|
__MockOwnableImplRoot_init();
|
||||||
|
_circuitVersion = 1;
|
||||||
|
emit HubInitialized();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Updates the registry address (simulates production function)
|
||||||
|
* @param registryAddress The new registry address
|
||||||
|
*/
|
||||||
|
function updateRegistry(address registryAddress) external onlyOwner {
|
||||||
|
_registry = registryAddress;
|
||||||
|
emit RegistryUpdated(registryAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Gets the circuit version
|
||||||
|
*/
|
||||||
|
function getCircuitVersion() external view returns (uint256) {
|
||||||
|
return _circuitVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Gets the registry address
|
||||||
|
*/
|
||||||
|
function getRegistry() external view returns (address) {
|
||||||
|
return _registry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Updates the circuit version
|
||||||
|
* @param version The new circuit version
|
||||||
|
*/
|
||||||
|
function updateCircuitVersion(uint256 version) external onlyOwner {
|
||||||
|
_circuitVersion = version;
|
||||||
|
}
|
||||||
|
}
|
||||||
31
contracts/contracts/tests/MockOwnableImplRoot.sol
Normal file
31
contracts/contracts/tests/MockOwnableImplRoot.sol
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
pragma solidity 0.8.28;
|
||||||
|
|
||||||
|
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
|
||||||
|
import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @title MockOwnableImplRoot
|
||||||
|
* @dev Mock contract that simulates the OLD production ImplRoot using Ownable2StepUpgradeable
|
||||||
|
* This represents what's currently deployed in production before the governance upgrade.
|
||||||
|
*/
|
||||||
|
abstract contract MockOwnableImplRoot is UUPSUpgradeable, Ownable2StepUpgradeable {
|
||||||
|
// Reserved storage space to allow for layout changes in the future.
|
||||||
|
uint256[50] private __gap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Initializes the contract by setting the deployer as the initial owner and initializing
|
||||||
|
* the UUPS proxy functionality.
|
||||||
|
*/
|
||||||
|
function __MockOwnableImplRoot_init() internal virtual onlyInitializing {
|
||||||
|
__Ownable_init(msg.sender);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Authorizes an upgrade to a new implementation.
|
||||||
|
* Requirements:
|
||||||
|
* - Must be called through a proxy.
|
||||||
|
* - Caller must be the owner.
|
||||||
|
*/
|
||||||
|
function _authorizeUpgrade(address newImplementation) internal virtual override onlyProxy onlyOwner {}
|
||||||
|
}
|
||||||
102
contracts/contracts/tests/MockOwnableRegistry.sol
Normal file
102
contracts/contracts/tests/MockOwnableRegistry.sol
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
pragma solidity 0.8.28;
|
||||||
|
|
||||||
|
import {MockOwnableImplRoot} from "./MockOwnableImplRoot.sol";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @title MockOwnableRegistry
|
||||||
|
* @dev Mock contract that simulates the OLD production Registry using Ownable
|
||||||
|
* This represents what's currently deployed in production before the governance upgrade.
|
||||||
|
*/
|
||||||
|
contract MockOwnableRegistry is MockOwnableImplRoot {
|
||||||
|
/// @notice Hub address
|
||||||
|
address private _hub;
|
||||||
|
|
||||||
|
/// @notice CSCA Root
|
||||||
|
bytes32 private _cscaRoot;
|
||||||
|
|
||||||
|
/// @notice Some registry data
|
||||||
|
mapping(bytes32 => bool) private _commitments;
|
||||||
|
|
||||||
|
/// @notice Event emitted when registry is initialized
|
||||||
|
event RegistryInitialized(address indexed hub);
|
||||||
|
|
||||||
|
/// @notice Event emitted when hub is updated
|
||||||
|
event HubUpdated(address indexed hub);
|
||||||
|
|
||||||
|
/// @notice Event emitted when CSCA root is updated
|
||||||
|
event CscaRootUpdated(bytes32 indexed cscaRoot);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Constructor that disables initializers for the implementation contract.
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
_disableInitializers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Initializes the Registry contract (simulates production initialization)
|
||||||
|
* @param hubAddress The hub address
|
||||||
|
*/
|
||||||
|
function initialize(address hubAddress) external initializer {
|
||||||
|
__MockOwnableImplRoot_init();
|
||||||
|
_hub = hubAddress;
|
||||||
|
emit RegistryInitialized(hubAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Sets the hub address (simulates production function)
|
||||||
|
* @param hubAddress The new hub address
|
||||||
|
*/
|
||||||
|
function setHub(address hubAddress) external onlyOwner {
|
||||||
|
_hub = hubAddress;
|
||||||
|
emit HubUpdated(hubAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Updates the hub address (simulates production function)
|
||||||
|
* @param hubAddress The new hub address
|
||||||
|
*/
|
||||||
|
function updateHub(address hubAddress) external onlyOwner {
|
||||||
|
_hub = hubAddress;
|
||||||
|
emit HubUpdated(hubAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Updates the CSCA root (simulates production function)
|
||||||
|
* @param cscaRoot The new CSCA root
|
||||||
|
*/
|
||||||
|
function updateCscaRoot(bytes32 cscaRoot) external onlyOwner {
|
||||||
|
_cscaRoot = cscaRoot;
|
||||||
|
emit CscaRootUpdated(cscaRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Adds a commitment (simulates production function)
|
||||||
|
* @param commitment The commitment to add
|
||||||
|
*/
|
||||||
|
function addCommitment(bytes32 commitment) external onlyOwner {
|
||||||
|
_commitments[commitment] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Gets the hub address
|
||||||
|
*/
|
||||||
|
function getHub() external view returns (address) {
|
||||||
|
return _hub;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Gets the CSCA root
|
||||||
|
*/
|
||||||
|
function getCscaRoot() external view returns (bytes32) {
|
||||||
|
return _cscaRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Checks if a commitment exists
|
||||||
|
*/
|
||||||
|
function hasCommitment(bytes32 commitment) external view returns (bool) {
|
||||||
|
return _commitments[commitment];
|
||||||
|
}
|
||||||
|
}
|
||||||
82
contracts/contracts/tests/MockUpgradedHub.sol
Normal file
82
contracts/contracts/tests/MockUpgradedHub.sol
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
pragma solidity 0.8.28;
|
||||||
|
|
||||||
|
import {ImplRoot} from "../upgradeable/ImplRoot.sol";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @title MockUpgradedHub
|
||||||
|
* @dev Mock contract that simulates the NEW Hub with AccessControl governance
|
||||||
|
* This represents what the Hub will look like after the governance upgrade.
|
||||||
|
*/
|
||||||
|
contract MockUpgradedHub is ImplRoot {
|
||||||
|
/// @notice Circuit version for compatibility
|
||||||
|
uint256 private _circuitVersion;
|
||||||
|
|
||||||
|
/// @notice Registry address
|
||||||
|
address private _registry;
|
||||||
|
|
||||||
|
/// @notice Event emitted when hub is initialized with governance
|
||||||
|
event HubGovernanceInitialized();
|
||||||
|
|
||||||
|
/// @notice Event emitted when registry is updated
|
||||||
|
event RegistryUpdated(address indexed registry);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Constructor that disables initializers for the implementation contract.
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
_disableInitializers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Initializes governance for the upgraded Hub
|
||||||
|
* This should be called after the upgrade to set up AccessControl
|
||||||
|
* NOTE: This ONLY initializes governance roles, does NOT modify existing state
|
||||||
|
*/
|
||||||
|
function initialize() external reinitializer(2) {
|
||||||
|
__ImplRoot_init();
|
||||||
|
// DO NOT modify _registry or _circuitVersion - they should be preserved from before upgrade!
|
||||||
|
emit HubGovernanceInitialized();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Updates the registry address (now requires SECURITY_ROLE)
|
||||||
|
* @param registryAddress The new registry address
|
||||||
|
*/
|
||||||
|
function updateRegistry(address registryAddress) external onlyRole(SECURITY_ROLE) {
|
||||||
|
_registry = registryAddress;
|
||||||
|
emit RegistryUpdated(registryAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Updates circuit version (requires SECURITY_ROLE)
|
||||||
|
* @param version The new circuit version
|
||||||
|
*/
|
||||||
|
function updateCircuitVersion(uint256 version) external onlyRole(SECURITY_ROLE) {
|
||||||
|
_circuitVersion = version;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Gets the circuit version
|
||||||
|
*/
|
||||||
|
function getCircuitVersion() external view returns (uint256) {
|
||||||
|
return _circuitVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Gets the registry address
|
||||||
|
*/
|
||||||
|
function getRegistry() external view returns (address) {
|
||||||
|
return _registry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Checks if the upgrade preserved critical storage data
|
||||||
|
* This is a verification function to ensure storage migration worked
|
||||||
|
*/
|
||||||
|
function verifyStorageMigration() external view returns (bool) {
|
||||||
|
// The important thing is that the contract is functional and governance works
|
||||||
|
// Registry and circuit version should be preserved, deprecated owner may be zero
|
||||||
|
return hasRole(SECURITY_ROLE, msg.sender) || hasRole(OPERATIONS_ROLE, msg.sender);
|
||||||
|
}
|
||||||
|
}
|
||||||
103
contracts/contracts/tests/MockUpgradedRegistry.sol
Normal file
103
contracts/contracts/tests/MockUpgradedRegistry.sol
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
pragma solidity 0.8.28;
|
||||||
|
|
||||||
|
import {ImplRoot} from "../upgradeable/ImplRoot.sol";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @title MockUpgradedRegistry
|
||||||
|
* @dev Mock contract that simulates the NEW Registry with AccessControl governance
|
||||||
|
* This represents what the Registry will look like after the governance upgrade.
|
||||||
|
*/
|
||||||
|
contract MockUpgradedRegistry is ImplRoot {
|
||||||
|
/// @notice Hub address
|
||||||
|
address private _hub;
|
||||||
|
|
||||||
|
/// @notice CSCA Root
|
||||||
|
bytes32 private _cscaRoot;
|
||||||
|
|
||||||
|
/// @notice Some registry data
|
||||||
|
mapping(bytes32 => bool) private _commitments;
|
||||||
|
|
||||||
|
/// @notice Event emitted when registry governance is initialized
|
||||||
|
event RegistryGovernanceInitialized();
|
||||||
|
|
||||||
|
/// @notice Event emitted when hub is updated
|
||||||
|
event HubUpdated(address indexed hub);
|
||||||
|
|
||||||
|
/// @notice Event emitted when CSCA root is updated
|
||||||
|
event CscaRootUpdated(bytes32 indexed cscaRoot);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Constructor that disables initializers for the implementation contract.
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
_disableInitializers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Initializes governance for the upgraded Registry
|
||||||
|
* This should be called after the upgrade to set up AccessControl
|
||||||
|
* NOTE: This ONLY initializes governance roles, does NOT modify existing state
|
||||||
|
*/
|
||||||
|
function initialize() external reinitializer(2) {
|
||||||
|
__ImplRoot_init();
|
||||||
|
// DO NOT modify _hub or _cscaRoot - they should be preserved from before upgrade!
|
||||||
|
emit RegistryGovernanceInitialized();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Sets the hub address (now requires SECURITY_ROLE)
|
||||||
|
* @param hubAddress The new hub address
|
||||||
|
*/
|
||||||
|
function setHub(address hubAddress) external onlyRole(SECURITY_ROLE) {
|
||||||
|
_hub = hubAddress;
|
||||||
|
emit HubUpdated(hubAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Updates the hub address (now requires SECURITY_ROLE)
|
||||||
|
* @param hubAddress The new hub address
|
||||||
|
*/
|
||||||
|
function updateHub(address hubAddress) external onlyRole(SECURITY_ROLE) {
|
||||||
|
_hub = hubAddress;
|
||||||
|
emit HubUpdated(hubAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Updates the CSCA root (now requires SECURITY_ROLE)
|
||||||
|
* @param cscaRoot The new CSCA root
|
||||||
|
*/
|
||||||
|
function updateCscaRoot(bytes32 cscaRoot) external onlyRole(OPERATIONS_ROLE) {
|
||||||
|
_cscaRoot = cscaRoot;
|
||||||
|
emit CscaRootUpdated(cscaRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Adds a commitment (now requires SECURITY_ROLE)
|
||||||
|
* @param commitment The commitment to add
|
||||||
|
*/
|
||||||
|
function addCommitment(bytes32 commitment) external onlyRole(SECURITY_ROLE) {
|
||||||
|
_commitments[commitment] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Gets the hub address
|
||||||
|
*/
|
||||||
|
function getHub() external view returns (address) {
|
||||||
|
return _hub;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Gets the CSCA root
|
||||||
|
*/
|
||||||
|
function getCscaRoot() external view returns (bytes32) {
|
||||||
|
return _cscaRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Checks if a commitment exists
|
||||||
|
*/
|
||||||
|
function hasCommitment(bytes32 commitment) external view returns (bool) {
|
||||||
|
return _commitments[commitment];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,15 +4,23 @@ pragma solidity 0.8.28;
|
|||||||
import {ImplRoot} from "../../contracts/upgradeable/ImplRoot.sol";
|
import {ImplRoot} from "../../contracts/upgradeable/ImplRoot.sol";
|
||||||
|
|
||||||
contract MockImplRoot is ImplRoot {
|
contract MockImplRoot is ImplRoot {
|
||||||
function exposed__ImplRoot_init() external {
|
function exposed__ImplRoot_init() external initializer {
|
||||||
__ImplRoot_init();
|
__ImplRoot_init();
|
||||||
}
|
}
|
||||||
|
|
||||||
function exposed__Ownable_init(address initialOwner) external initializer {
|
|
||||||
__Ownable_init(initialOwner);
|
|
||||||
}
|
|
||||||
|
|
||||||
function exposed_authorizeUpgrade(address newImplementation) external {
|
function exposed_authorizeUpgrade(address newImplementation) external {
|
||||||
_authorizeUpgrade(newImplementation);
|
_authorizeUpgrade(newImplementation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function exposed_grantRole(bytes32 role, address account) external {
|
||||||
|
_grantRole(role, account);
|
||||||
|
}
|
||||||
|
|
||||||
|
function exposed_revokeRole(bytes32 role, address account) external {
|
||||||
|
_revokeRole(role, account);
|
||||||
|
}
|
||||||
|
|
||||||
|
function exposed_setRoleAdmin(bytes32 role, bytes32 adminRole) external {
|
||||||
|
_setRoleAdmin(role, adminRole);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ contract testUpgradedIdentityVerificationHubImplV1 is
|
|||||||
* @param isTestInput Boolean value which shows it is test or not
|
* @param isTestInput Boolean value which shows it is test or not
|
||||||
*/
|
*/
|
||||||
function initialize(bool isTestInput) external reinitializer(3) {
|
function initialize(bool isTestInput) external reinitializer(3) {
|
||||||
__ImplRoot_init();
|
__Ownable_init(msg.sender);
|
||||||
_isTest = isTestInput;
|
_isTest = isTestInput;
|
||||||
emit TestHubInitialized();
|
emit TestHubInitialized();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,25 @@
|
|||||||
pragma solidity 0.8.28;
|
pragma solidity 0.8.28;
|
||||||
|
|
||||||
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
|
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
|
||||||
import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol";
|
import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @title ImplRoot
|
* @title ImplRoot
|
||||||
* @dev Abstract contract providing upgradeable functionality via UUPSUpgradeable,
|
* @dev Abstract contract providing upgradeable functionality via UUPSUpgradeable,
|
||||||
* along with a two-step ownable mechanism using Ownable2StepUpgradeable.
|
* along with role-based access control using AccessControlUpgradeable.
|
||||||
* Serves as a base for upgradeable implementations.
|
* Serves as a base for upgradeable implementations.
|
||||||
|
*
|
||||||
|
* Governance Roles:
|
||||||
|
* - SECURITY_ROLE: Security-sensitive operations and role management (3/5 multisig consensus)
|
||||||
|
* - OPERATIONS_ROLE: Routine operational tasks (2/5 multisig consensus)
|
||||||
*/
|
*/
|
||||||
abstract contract ImplRoot is UUPSUpgradeable, Ownable2StepUpgradeable {
|
abstract contract ImplRoot is UUPSUpgradeable, AccessControlUpgradeable {
|
||||||
|
/// @notice Security-sensitive operations requiring 3/5 multisig consensus
|
||||||
|
bytes32 public constant SECURITY_ROLE = keccak256("SECURITY_ROLE");
|
||||||
|
|
||||||
|
/// @notice Routine operations requiring 2/5 multisig consensus
|
||||||
|
bytes32 public constant OPERATIONS_ROLE = keccak256("OPERATIONS_ROLE");
|
||||||
|
|
||||||
// Reserved storage space to allow for layout changes in the future.
|
// Reserved storage space to allow for layout changes in the future.
|
||||||
uint256[50] private __gap;
|
uint256[50] private __gap;
|
||||||
|
|
||||||
@@ -21,17 +31,23 @@ abstract contract ImplRoot is UUPSUpgradeable, Ownable2StepUpgradeable {
|
|||||||
* This function should be called in the initializer of the derived contract.
|
* This function should be called in the initializer of the derived contract.
|
||||||
*/
|
*/
|
||||||
function __ImplRoot_init() internal virtual onlyInitializing {
|
function __ImplRoot_init() internal virtual onlyInitializing {
|
||||||
__Ownable_init(msg.sender);
|
__AccessControl_init();
|
||||||
__UUPSUpgradeable_init();
|
|
||||||
|
_grantRole(SECURITY_ROLE, msg.sender);
|
||||||
|
_grantRole(OPERATIONS_ROLE, msg.sender);
|
||||||
|
|
||||||
|
// Set role admins - SECURITY_ROLE manages all roles
|
||||||
|
_setRoleAdmin(SECURITY_ROLE, SECURITY_ROLE);
|
||||||
|
_setRoleAdmin(OPERATIONS_ROLE, SECURITY_ROLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @dev Authorizes an upgrade to a new implementation.
|
* @dev Authorizes an upgrade to a new implementation.
|
||||||
* Requirements:
|
* Requirements:
|
||||||
* - Must be called through a proxy.
|
* - Must be called through a proxy.
|
||||||
* - Caller must be the contract owner.
|
* - Caller must have SECURITY_ROLE.
|
||||||
*
|
*
|
||||||
* @param newImplementation The address of the new implementation contract.
|
* @param newImplementation The address of the new implementation contract.
|
||||||
*/
|
*/
|
||||||
function _authorizeUpgrade(address newImplementation) internal virtual override onlyProxy onlyOwner {}
|
function _authorizeUpgrade(address newImplementation) internal virtual override onlyProxy onlyRole(SECURITY_ROLE) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,32 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
pragma solidity 0.8.28;
|
pragma solidity 0.8.28;
|
||||||
|
|
||||||
import "@openzeppelin/contracts/access/Ownable.sol";
|
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @title PCR0Manager
|
* @title PCR0Manager
|
||||||
* @notice This contract manages a mapping of PCR0 values (provided as a 48-byte value)
|
* @notice This contract manages a mapping of PCR0 values (provided as a 48-byte value)
|
||||||
* to booleans. The PCR0 value (the 48-byte SHA384 output) is hashed
|
* to booleans. The PCR0 value (the 48-byte SHA384 output) is hashed
|
||||||
* using keccak256 and then stored in the mapping.
|
* using keccak256 and then stored in the mapping.
|
||||||
* Only the owner can add or remove entries.
|
* Only accounts with SECURITY_ROLE can add or remove entries.
|
||||||
|
* @custom:version 1.2.0
|
||||||
*/
|
*/
|
||||||
contract PCR0Manager is Ownable {
|
contract PCR0Manager is AccessControl {
|
||||||
// Pass msg.sender directly to Ownable constructor
|
/// @notice Critical operations and role management requiring 3/5 multisig consensus
|
||||||
constructor() Ownable(msg.sender) {}
|
bytes32 public constant SECURITY_ROLE = keccak256("SECURITY_ROLE");
|
||||||
|
|
||||||
|
/// @notice Standard operations requiring 2/5 multisig consensus
|
||||||
|
bytes32 public constant OPERATIONS_ROLE = keccak256("OPERATIONS_ROLE");
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// Grant all roles to deployer initially
|
||||||
|
_grantRole(SECURITY_ROLE, msg.sender);
|
||||||
|
_grantRole(OPERATIONS_ROLE, msg.sender);
|
||||||
|
|
||||||
|
// Set role admins - SECURITY_ROLE is admin of both roles
|
||||||
|
_setRoleAdmin(SECURITY_ROLE, SECURITY_ROLE);
|
||||||
|
_setRoleAdmin(OPERATIONS_ROLE, SECURITY_ROLE);
|
||||||
|
}
|
||||||
|
|
||||||
// Mapping from keccak256(pcr0) to its boolean state.
|
// Mapping from keccak256(pcr0) to its boolean state.
|
||||||
mapping(bytes32 => bool) public pcr0Mapping;
|
mapping(bytes32 => bool) public pcr0Mapping;
|
||||||
@@ -27,12 +41,14 @@ contract PCR0Manager is Ownable {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @notice Adds a new PCR0 entry by setting its value to true.
|
* @notice Adds a new PCR0 entry by setting its value to true.
|
||||||
* @param pcr0 The PCR0 value (must be exactly 48 bytes).
|
* @param pcr0 The PCR0 value (must be exactly 32 bytes).
|
||||||
* @dev Reverts if the PCR0 value is not 48 bytes or if it is already set.
|
* @dev Reverts if the PCR0 value is not 32 bytes or if it is already set.
|
||||||
|
* @dev Pads the PCR0 value to 48 bytes by prefixing 16 zero bytes to maintain mobile app compatibility.
|
||||||
*/
|
*/
|
||||||
function addPCR0(bytes calldata pcr0) external onlyOwner {
|
function addPCR0(bytes calldata pcr0) external onlyRole(SECURITY_ROLE) {
|
||||||
require(pcr0.length == 48, "PCR0 must be 48 bytes");
|
require(pcr0.length == 32, "PCR0 must be 32 bytes");
|
||||||
bytes32 key = keccak256(pcr0);
|
bytes memory paddedPcr0 = abi.encodePacked(new bytes(16), pcr0);
|
||||||
|
bytes32 key = keccak256(paddedPcr0);
|
||||||
require(!pcr0Mapping[key], "PCR0 already set");
|
require(!pcr0Mapping[key], "PCR0 already set");
|
||||||
pcr0Mapping[key] = true;
|
pcr0Mapping[key] = true;
|
||||||
emit PCR0Added(key);
|
emit PCR0Added(key);
|
||||||
@@ -40,12 +56,14 @@ contract PCR0Manager is Ownable {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @notice Removes an existing PCR0 entry by setting its value to false.
|
* @notice Removes an existing PCR0 entry by setting its value to false.
|
||||||
* @param pcr0 The PCR0 value (must be exactly 48 bytes).
|
* @param pcr0 The PCR0 value (must be exactly 32 bytes).
|
||||||
* @dev Reverts if the PCR0 value is not 48 bytes or if it is not currently set.
|
* @dev Reverts if the PCR0 value is not 32 bytes or if it is not currently set.
|
||||||
|
* @dev Pads the PCR0 value to 48 bytes by prefixing 16 zero bytes to maintain mobile app compatibility.
|
||||||
*/
|
*/
|
||||||
function removePCR0(bytes calldata pcr0) external onlyOwner {
|
function removePCR0(bytes calldata pcr0) external onlyRole(SECURITY_ROLE) {
|
||||||
require(pcr0.length == 48, "PCR0 must be 48 bytes");
|
require(pcr0.length == 32, "PCR0 must be 32 bytes");
|
||||||
bytes32 key = keccak256(pcr0);
|
bytes memory paddedPcr0 = abi.encodePacked(new bytes(16), pcr0);
|
||||||
|
bytes32 key = keccak256(paddedPcr0);
|
||||||
require(pcr0Mapping[key], "PCR0 not set");
|
require(pcr0Mapping[key], "PCR0 not set");
|
||||||
pcr0Mapping[key] = false;
|
pcr0Mapping[key] = false;
|
||||||
emit PCR0Removed(key);
|
emit PCR0Removed(key);
|
||||||
@@ -54,6 +72,8 @@ contract PCR0Manager is Ownable {
|
|||||||
/**
|
/**
|
||||||
* @notice Checks whether a given PCR0 value is set to true in the mapping.
|
* @notice Checks whether a given PCR0 value is set to true in the mapping.
|
||||||
* @param pcr0 The PCR0 value (must be exactly 48 bytes).
|
* @param pcr0 The PCR0 value (must be exactly 48 bytes).
|
||||||
|
* @dev Does not pad the PCR0 value as this is handled by the mobile app.
|
||||||
|
* @dev If you are manually calling this function, you need to pad the PCR0 value to 48 bytes, prefixing 16 zero bytes.
|
||||||
* @return exists True if the PCR0 entry is set, false otherwise.
|
* @return exists True if the PCR0 entry is set, false otherwise.
|
||||||
*/
|
*/
|
||||||
function isPCR0Set(bytes calldata pcr0) external view returns (bool exists) {
|
function isPCR0Set(bytes calldata pcr0) external view returns (bool exists) {
|
||||||
|
|||||||
@@ -37,25 +37,25 @@ contract Verifier_register_aadhaar {
|
|||||||
uint256 constant gammax2 = 10857046999023057135944570762232829481370756359578518086990519993285655852781;
|
uint256 constant gammax2 = 10857046999023057135944570762232829481370756359578518086990519993285655852781;
|
||||||
uint256 constant gammay1 = 4082367875863433681332203403145435568316851327593401208105741076214120093531;
|
uint256 constant gammay1 = 4082367875863433681332203403145435568316851327593401208105741076214120093531;
|
||||||
uint256 constant gammay2 = 8495653923123431417604973247489272438418190587263600148770280649306958101930;
|
uint256 constant gammay2 = 8495653923123431417604973247489272438418190587263600148770280649306958101930;
|
||||||
uint256 constant deltax1 = 1184175006002790631176821634090938467107330227007158853824891629496015889924;
|
uint256 constant deltax1 = 3953219198104570901098823830840773856017689139278458081183220490752145815050;
|
||||||
uint256 constant deltax2 = 12086636205582787465813058141825079064824697543086779109775595053805081617827;
|
uint256 constant deltax2 = 428186582661072144108009098107578252463491462238432931497262180014713596115;
|
||||||
uint256 constant deltay1 = 4456837667197728326322115376478122146150647259307011732553476664405503785753;
|
uint256 constant deltay1 = 18162968189172780580333095558539690618880186036957545736311404283407493778880;
|
||||||
uint256 constant deltay2 = 9088696651190771223855139438876954166862164661620992858425695135876196457926;
|
uint256 constant deltay2 = 7343682947937413219111184190299798376421430633278379635221606345245958931239;
|
||||||
|
|
||||||
uint256 constant IC0x = 6547380589242664979389953612506618657067204598675122139604885565320676833158;
|
uint256 constant IC0x = 18984838814932147425072354846429508676387686524229308734161716095463360490134;
|
||||||
uint256 constant IC0y = 19055399919951028177234969337049077818155869440497248883170998389487338107126;
|
uint256 constant IC0y = 11220857659665071811279473081460089783437970319349511357818317231596300603739;
|
||||||
|
|
||||||
uint256 constant IC1x = 20557545828033851521979343305884318041481443328161582179150888164584749744669;
|
uint256 constant IC1x = 20557545828033851521979343305884318041481443328161582179150888164584749744669;
|
||||||
uint256 constant IC1y = 21560118189953885636148717201222479281100786469743463492679572665614931385205;
|
uint256 constant IC1y = 21560118189953885636148717201222479281100786469743463492679572665614931385205;
|
||||||
|
|
||||||
uint256 constant IC2x = 17559551632997878871440402139938294429514970824368869332125462241643052815376;
|
uint256 constant IC2x = 16679737504993527028036863898232919844061144900682159566005073982271081014169;
|
||||||
uint256 constant IC2y = 18428425902276807983388946110037886804676016275275246286544615654725514849838;
|
uint256 constant IC2y = 608284743266912406546568650108359232826801114144551290914053108404596136834;
|
||||||
|
|
||||||
uint256 constant IC3x = 18768989044514693938417600792629717603460465495191187242290958821278680606604;
|
uint256 constant IC3x = 10860675204793746311158823740347274485680296774011483979610282012223174969615;
|
||||||
uint256 constant IC3y = 6584358559179261704032830455997936799129839324733806160004605275139747821694;
|
uint256 constant IC3y = 15029247271645078761075880233744321449871203863194823447037797284218806524473;
|
||||||
|
|
||||||
uint256 constant IC4x = 16692378542219000347024593964346873649905710163948976095790586330709671710647;
|
uint256 constant IC4x = 17894574390662839711557891944994831304061930223868407277717041388786423798517;
|
||||||
uint256 constant IC4y = 2622311591517607336391164955074698243697841582935873217110527812716210930596;
|
uint256 constant IC4y = 10747262533817845366080322542335489573224940900925614580771284497946454436591;
|
||||||
|
|
||||||
// Memory data
|
// Memory data
|
||||||
uint16 constant pVk = 0;
|
uint16 constant pVk = 0;
|
||||||
|
|||||||
225
contracts/deployments/registry.json
Normal file
225
contracts/deployments/registry.json
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./registry.schema.json",
|
||||||
|
"lastUpdated": "2025-12-10T06:17:50.863Z",
|
||||||
|
"contracts": {
|
||||||
|
"IdentityVerificationHub": {
|
||||||
|
"source": "IdentityVerificationHubImplV2",
|
||||||
|
"type": "uups-proxy",
|
||||||
|
"description": "Main identity verification hub for all document types"
|
||||||
|
},
|
||||||
|
"IdentityRegistry": {
|
||||||
|
"source": "IdentityRegistryImplV1",
|
||||||
|
"type": "uups-proxy",
|
||||||
|
"description": "Passport identity registry"
|
||||||
|
},
|
||||||
|
"IdentityRegistryIdCard": {
|
||||||
|
"source": "IdentityRegistryIdCardImplV1",
|
||||||
|
"type": "uups-proxy",
|
||||||
|
"description": "EU ID Card identity registry"
|
||||||
|
},
|
||||||
|
"IdentityRegistryAadhaar": {
|
||||||
|
"source": "IdentityRegistryAadhaarImplV1",
|
||||||
|
"type": "uups-proxy",
|
||||||
|
"description": "Aadhaar identity registry"
|
||||||
|
},
|
||||||
|
"PCR0Manager": {
|
||||||
|
"source": "PCR0Manager",
|
||||||
|
"type": "non-upgradeable",
|
||||||
|
"description": "PCR0 value management for TEE verification"
|
||||||
|
},
|
||||||
|
"VerifyAll": {
|
||||||
|
"source": "VerifyAll",
|
||||||
|
"type": "non-upgradeable",
|
||||||
|
"description": "SDK verification helper contract"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"networks": {
|
||||||
|
"celo": {
|
||||||
|
"chainId": 42220,
|
||||||
|
"governance": {
|
||||||
|
"securityMultisig": "0x738f0bb37FD3b6C4Cdf8eb6FcdFaAA0CA208CB4A",
|
||||||
|
"operationsMultisig": "0x067b18e09A10Fa03d027c1D60A098CEbbE5637f0",
|
||||||
|
"securityThreshold": "3/5",
|
||||||
|
"operationsThreshold": "2/5"
|
||||||
|
},
|
||||||
|
"deployments": {
|
||||||
|
"IdentityVerificationHub": {
|
||||||
|
"proxy": "0xe57F4773bd9c9d8b6Cd70431117d353298B9f5BF",
|
||||||
|
"currentVersion": "2.12.0",
|
||||||
|
"currentImpl": "0x05FB9D7830889cc389E88198f6A224eA87F01151"
|
||||||
|
},
|
||||||
|
"IdentityRegistry": {
|
||||||
|
"proxy": "0x37F5CB8cB1f6B00aa768D8aA99F1A9289802A968",
|
||||||
|
"currentVersion": "1.2.0",
|
||||||
|
"currentImpl": "0x81E7F74560FAF7eE8DE3a36A5a68B6cbc429Cd36"
|
||||||
|
},
|
||||||
|
"IdentityRegistryIdCard": {
|
||||||
|
"proxy": "0xeAD1E6Ec29c1f3D33a0662f253a3a94D189566E1",
|
||||||
|
"currentVersion": "1.2.0",
|
||||||
|
"currentImpl": "0x7d5e4b7D4c3029aF134D50642674Af8F875118a4"
|
||||||
|
},
|
||||||
|
"IdentityRegistryAadhaar": {
|
||||||
|
"proxy": "0xd603Fa8C8f4694E8DD1DcE1f27C0C3fc91e32Ac4",
|
||||||
|
"currentVersion": "1.2.0",
|
||||||
|
"currentImpl": "0xbD861A9cecf7B0A9631029d55A8CE1155e50697c"
|
||||||
|
},
|
||||||
|
"PCR0Manager": {
|
||||||
|
"address": "0x9743fe2C1c3D2b068c56dE314e9B10DA9c904717",
|
||||||
|
"currentVersion": "1.2.0"
|
||||||
|
},
|
||||||
|
"VerifyAll": {
|
||||||
|
"address": "",
|
||||||
|
"currentVersion": "1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"localhost": {
|
||||||
|
"chainId": 31337,
|
||||||
|
"governance": {
|
||||||
|
"securityMultisig": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
|
||||||
|
"operationsMultisig": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
|
||||||
|
"securityThreshold": "1/1",
|
||||||
|
"operationsThreshold": "1/1"
|
||||||
|
},
|
||||||
|
"deployments": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"versions": {
|
||||||
|
"IdentityVerificationHub": {
|
||||||
|
"2.12.0": {
|
||||||
|
"initializerVersion": 12,
|
||||||
|
"initializerFunction": "initializeGovernance",
|
||||||
|
"changelog": "Governance upgrade - migrated to AccessControlUpgradeable with multi-tier governance",
|
||||||
|
"gitTag": "hub-v2.12.0",
|
||||||
|
"deployments": {
|
||||||
|
"celo": {
|
||||||
|
"impl": "0x05FB9D7830889cc389E88198f6A224eA87F01151",
|
||||||
|
"deployedAt": "2025-12-10T05:43:58.258Z",
|
||||||
|
"deployedBy": "0xCaEe7aAF115F04D836E2D362A7c07F04db436bd0",
|
||||||
|
"gitCommit": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"2.11.0": {
|
||||||
|
"initializerVersion": 11,
|
||||||
|
"initializerFunction": "initialize",
|
||||||
|
"changelog": "V2 hub deployment with Ownable2StepUpgradeable governance",
|
||||||
|
"gitTag": "hub-v2.11.0",
|
||||||
|
"deployments": {
|
||||||
|
"celo": {
|
||||||
|
"impl": "",
|
||||||
|
"deployedAt": "",
|
||||||
|
"deployedBy": "",
|
||||||
|
"gitCommit": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"IdentityRegistry": {
|
||||||
|
"1.2.0": {
|
||||||
|
"initializerVersion": 2,
|
||||||
|
"initializerFunction": "initializeGovernance",
|
||||||
|
"changelog": "Governance upgrade - migrated to AccessControlUpgradeable",
|
||||||
|
"gitTag": "registry-passport-v1.2.0",
|
||||||
|
"deployments": {
|
||||||
|
"celo": {
|
||||||
|
"impl": "0x81E7F74560FAF7eE8DE3a36A5a68B6cbc429Cd36",
|
||||||
|
"deployedAt": "2025-12-10T05:53:12.534Z",
|
||||||
|
"deployedBy": "0xCaEe7aAF115F04D836E2D362A7c07F04db436bd0",
|
||||||
|
"gitCommit": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"initializerVersion": 1,
|
||||||
|
"initializerFunction": "initialize",
|
||||||
|
"changelog": "Initial deployment with Ownable2StepUpgradeable governance",
|
||||||
|
"gitTag": "registry-passport-v1.1.0",
|
||||||
|
"deployments": {
|
||||||
|
"celo": {
|
||||||
|
"impl": "",
|
||||||
|
"deployedAt": "",
|
||||||
|
"deployedBy": "",
|
||||||
|
"gitCommit": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"IdentityRegistryIdCard": {
|
||||||
|
"1.2.0": {
|
||||||
|
"initializerVersion": 2,
|
||||||
|
"initializerFunction": "initializeGovernance",
|
||||||
|
"changelog": "Governance upgrade - migrated to AccessControlUpgradeable",
|
||||||
|
"gitTag": "registry-idcard-v1.2.0",
|
||||||
|
"deployments": {
|
||||||
|
"celo": {
|
||||||
|
"impl": "0x7d5e4b7D4c3029aF134D50642674Af8F875118a4",
|
||||||
|
"deployedAt": "2025-12-10T05:45:56.772Z",
|
||||||
|
"deployedBy": "0xCaEe7aAF115F04D836E2D362A7c07F04db436bd0",
|
||||||
|
"gitCommit": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"initializerVersion": 1,
|
||||||
|
"initializerFunction": "initialize",
|
||||||
|
"changelog": "Initial deployment",
|
||||||
|
"gitTag": "registry-idcard-v1.1.0",
|
||||||
|
"deployments": {
|
||||||
|
"celo": {
|
||||||
|
"impl": "",
|
||||||
|
"deployedAt": "",
|
||||||
|
"deployedBy": "",
|
||||||
|
"gitCommit": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"IdentityRegistryAadhaar": {
|
||||||
|
"1.2.0": {
|
||||||
|
"initializerVersion": 2,
|
||||||
|
"initializerFunction": "initializeGovernance",
|
||||||
|
"changelog": "Governance upgrade - migrated to AccessControlUpgradeable",
|
||||||
|
"gitTag": "registry-aadhaar-v1.2.0",
|
||||||
|
"deployments": {
|
||||||
|
"celo": {
|
||||||
|
"impl": "0xbD861A9cecf7B0A9631029d55A8CE1155e50697c",
|
||||||
|
"deployedAt": "2025-12-10T05:47:22.844Z",
|
||||||
|
"deployedBy": "0xCaEe7aAF115F04D836E2D362A7c07F04db436bd0",
|
||||||
|
"gitCommit": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"1.1.0": {
|
||||||
|
"initializerVersion": 1,
|
||||||
|
"initializerFunction": "initialize",
|
||||||
|
"changelog": "Initial deployment",
|
||||||
|
"gitTag": "registry-aadhaar-v1.1.0",
|
||||||
|
"deployments": {
|
||||||
|
"celo": {
|
||||||
|
"impl": "",
|
||||||
|
"deployedAt": "",
|
||||||
|
"deployedBy": "",
|
||||||
|
"gitCommit": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"PCR0Manager": {
|
||||||
|
"1.2.0": {
|
||||||
|
"initializerVersion": 0,
|
||||||
|
"initializerFunction": "",
|
||||||
|
"changelog": "Multisig governance deployment - migrated from single owner to AccessControl",
|
||||||
|
"gitTag": "pcr0manager-v1.2.0",
|
||||||
|
"deployments": {
|
||||||
|
"celo": {
|
||||||
|
"impl": "0x9743fe2C1c3D2b068c56dE314e9B10DA9c904717",
|
||||||
|
"deployedAt": "2025-12-10T06:17:50.863Z",
|
||||||
|
"deployedBy": "0xCaEe7aAF115F04D836E2D362A7c07F04db436bd0",
|
||||||
|
"gitCommit": "5787cff3bcbea870b50eccd7164fbd45b758568e"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
54
contracts/foundry.toml
Normal file
54
contracts/foundry.toml
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# Foundry Configuration for Hardhat Compatibility
|
||||||
|
# Based on: https://getfoundry.sh/config/hardhat/
|
||||||
|
|
||||||
|
[profile.default]
|
||||||
|
# Use Hardhat's directory structure
|
||||||
|
src = "contracts"
|
||||||
|
out = "out" # Keep separate from Hardhat's artifacts to avoid conflicts
|
||||||
|
libs = ["node_modules", "lib"]
|
||||||
|
test = "test/foundry"
|
||||||
|
script = "script"
|
||||||
|
cache_path = "cache_forge"
|
||||||
|
|
||||||
|
# Enable FFI for OpenZeppelin Upgrades plugin
|
||||||
|
ffi = true
|
||||||
|
ast = true
|
||||||
|
build_info = true
|
||||||
|
extra_output = ["storageLayout"]
|
||||||
|
build_info_path = "out/build-info"
|
||||||
|
|
||||||
|
# Solidity compiler settings (match Hardhat)
|
||||||
|
solc_version = "0.8.28"
|
||||||
|
optimizer = true
|
||||||
|
optimizer_runs = 200
|
||||||
|
via_ir = false
|
||||||
|
evm_version = "cancun"
|
||||||
|
|
||||||
|
# File system permissions for OpenZeppelin plugin
|
||||||
|
fs_permissions = [{ access = "read", path = "out" }]
|
||||||
|
|
||||||
|
# Linked libraries (deployed on Celo Mainnet)
|
||||||
|
libraries = [
|
||||||
|
"contracts/libraries/CustomVerifier.sol:CustomVerifier:0x9E66B82Da87309fAE1403078d498a069A30860c4",
|
||||||
|
"node_modules/poseidon-solidity/PoseidonT3.sol:PoseidonT3:0xF134707a4C4a3a76b8410fC0294d620A7c341581"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Celo mainnet and testnet RPC endpoints
|
||||||
|
[rpc_endpoints]
|
||||||
|
celo = "${CELO_RPC_URL}"
|
||||||
|
celo_alfajores = "https://alfajores-forno.celo-testnet.org"
|
||||||
|
|
||||||
|
# Etherscan API configuration for contract verification
|
||||||
|
[etherscan]
|
||||||
|
celo = { key = "${CELOSCAN_API_KEY}", url = "https://api.celoscan.io/api" }
|
||||||
|
celo_alfajores = { key = "${CELOSCAN_API_KEY}", url = "https://api-alfajores.celoscan.io/api" }
|
||||||
|
|
||||||
|
# Formatting settings
|
||||||
|
[fmt]
|
||||||
|
line_length = 120
|
||||||
|
tab_width = 4
|
||||||
|
bracket_spacing = true
|
||||||
|
int_types = "long"
|
||||||
|
multiline_func_header = "all"
|
||||||
|
quote_style = "double"
|
||||||
|
number_underscore = "thousands"
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
import { HardhatUserConfig } from "hardhat/config";
|
import { HardhatUserConfig } from "hardhat/config";
|
||||||
import "@nomicfoundation/hardhat-toolbox";
|
import "@nomicfoundation/hardhat-toolbox";
|
||||||
|
import "@openzeppelin/hardhat-upgrades";
|
||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
import "hardhat-contract-sizer";
|
import "hardhat-contract-sizer";
|
||||||
import "@nomicfoundation/hardhat-ignition-ethers";
|
import "@nomicfoundation/hardhat-ignition-ethers";
|
||||||
import "solidity-coverage";
|
import "solidity-coverage";
|
||||||
import "hardhat-gas-reporter";
|
import "hardhat-gas-reporter";
|
||||||
import "hardhat-contract-sizer";
|
|
||||||
|
// Import custom upgrade tasks
|
||||||
|
import "./tasks/upgrade";
|
||||||
|
|
||||||
// Use a dummy private key for CI/local development (not used for actual deployments)
|
// Use a dummy private key for CI/local development (not used for actual deployments)
|
||||||
const DUMMY_PRIVATE_KEY = "0x0000000000000000000000000000000000000000000000000000000000000001";
|
const DUMMY_PRIVATE_KEY = "0x0000000000000000000000000000000000000000000000000000000000000001";
|
||||||
@@ -16,9 +19,10 @@ const config: HardhatUserConfig = {
|
|||||||
solidity: {
|
solidity: {
|
||||||
version: "0.8.28",
|
version: "0.8.28",
|
||||||
settings: {
|
settings: {
|
||||||
|
evmVersion: "cancun",
|
||||||
optimizer: {
|
optimizer: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
runs: 100000,
|
runs: 200,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -37,7 +41,7 @@ const config: HardhatUserConfig = {
|
|||||||
chainId: 31337,
|
chainId: 31337,
|
||||||
url: "http://127.0.0.1:8545",
|
url: "http://127.0.0.1:8545",
|
||||||
accounts: {
|
accounts: {
|
||||||
mnemonic: "test test test test test test test test test test test test",
|
mnemonic: "test test test test test test test test test test test junk",
|
||||||
count: 20,
|
count: 20,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
1
contracts/lib/forge-std
Submodule
1
contracts/lib/forge-std
Submodule
Submodule contracts/lib/forge-std added at 8e40513d67
1
contracts/lib/openzeppelin-foundry-upgrades
Submodule
1
contracts/lib/openzeppelin-foundry-upgrades
Submodule
Submodule contracts/lib/openzeppelin-foundry-upgrades added at cbce1e0030
@@ -71,7 +71,10 @@
|
|||||||
"update:ofacroot": "npx dotenv-cli -- bash -c 'NETWORK=${NETWORK} npx tsx scripts/updateRegistryOfacRoot.ts'",
|
"update:ofacroot": "npx dotenv-cli -- bash -c 'NETWORK=${NETWORK} npx tsx scripts/updateRegistryOfacRoot.ts'",
|
||||||
"update:pcr0": "npx dotenv-cli -- bash -c 'PCR0_ACTION=${PCR0_ACTION:-add} PCR0_KEY=${PCR0_KEY} yarn hardhat ignition deploy ignition/modules/scripts/updatePCR0.ts --network ${NETWORK:-localhost} --reset'",
|
"update:pcr0": "npx dotenv-cli -- bash -c 'PCR0_ACTION=${PCR0_ACTION:-add} PCR0_KEY=${PCR0_KEY} yarn hardhat ignition deploy ignition/modules/scripts/updatePCR0.ts --network ${NETWORK:-localhost} --reset'",
|
||||||
"upgrade:hub": "npx dotenv-cli -- bash -c 'yarn hardhat ignition deploy ignition/modules/upgrade/deployNewHubAndUpgrade.ts --network ${NETWORK:-localhost} ${VERIFY:+--verify}'",
|
"upgrade:hub": "npx dotenv-cli -- bash -c 'yarn hardhat ignition deploy ignition/modules/upgrade/deployNewHubAndUpgrade.ts --network ${NETWORK:-localhost} ${VERIFY:+--verify}'",
|
||||||
"upgrade:registry": "npx dotenv-cli -- bash -c 'yarn hardhat ignition deploy ignition/modules/upgrade/deployNewRegistryAndUpgrade.ts --network ${NETWORK:-localhost} ${VERIFY:+--verify}'"
|
"upgrade:registry": "npx dotenv-cli -- bash -c 'yarn hardhat ignition deploy ignition/modules/upgrade/deployNewRegistryAndUpgrade.ts --network ${NETWORK:-localhost} ${VERIFY:+--verify}'",
|
||||||
|
"upgrade": "npx dotenv-cli -- bash -c 'yarn hardhat upgrade --network ${NETWORK:-localhost}'",
|
||||||
|
"upgrade:status": "npx dotenv-cli -- bash -c 'yarn hardhat upgrade:status --network ${NETWORK:-localhost}'",
|
||||||
|
"upgrade:history": "yarn hardhat upgrade:history"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ashpect/smt": "https://github.com/ashpect/smt#main",
|
"@ashpect/smt": "https://github.com/ashpect/smt#main",
|
||||||
@@ -81,6 +84,9 @@
|
|||||||
"@openpassport/zk-kit-smt": "^0.0.1",
|
"@openpassport/zk-kit-smt": "^0.0.1",
|
||||||
"@openzeppelin/contracts": "5.4.0",
|
"@openzeppelin/contracts": "5.4.0",
|
||||||
"@openzeppelin/contracts-upgradeable": "5.4.0",
|
"@openzeppelin/contracts-upgradeable": "5.4.0",
|
||||||
|
"@safe-global/api-kit": "^4.0.1",
|
||||||
|
"@safe-global/protocol-kit": "^6.1.2",
|
||||||
|
"@safe-global/safe-core-sdk-types": "^5.1.0",
|
||||||
"@selfxyz/common": "workspace:^",
|
"@selfxyz/common": "workspace:^",
|
||||||
"@zk-kit/imt": "^2.0.0-beta.4",
|
"@zk-kit/imt": "^2.0.0-beta.4",
|
||||||
"@zk-kit/imt.sol": "^2.0.0-beta.12",
|
"@zk-kit/imt.sol": "^2.0.0-beta.12",
|
||||||
@@ -103,6 +109,7 @@
|
|||||||
"@nomicfoundation/hardhat-toolbox": "^3.0.0",
|
"@nomicfoundation/hardhat-toolbox": "^3.0.0",
|
||||||
"@nomicfoundation/hardhat-verify": "^2.0.6",
|
"@nomicfoundation/hardhat-verify": "^2.0.6",
|
||||||
"@nomicfoundation/ignition-core": "^0.15.12",
|
"@nomicfoundation/ignition-core": "^0.15.12",
|
||||||
|
"@openzeppelin/hardhat-upgrades": "^3.9.1",
|
||||||
"@typechain/ethers-v6": "^0.4.3",
|
"@typechain/ethers-v6": "^0.4.3",
|
||||||
"@typechain/hardhat": "^8.0.3",
|
"@typechain/hardhat": "^8.0.3",
|
||||||
"@types/chai": "^4.3.16",
|
"@types/chai": "^4.3.16",
|
||||||
|
|||||||
4
contracts/remappings.txt
Normal file
4
contracts/remappings.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/
|
||||||
|
@openzeppelin/contracts-upgradeable/=node_modules/@openzeppelin/contracts-upgradeable/
|
||||||
|
forge-std/=lib/forge-std/src/
|
||||||
|
openzeppelin-foundry-upgrades/=lib/openzeppelin-foundry-upgrades/src/
|
||||||
157
contracts/script/MigratePCR0Manager.s.sol
Normal file
157
contracts/script/MigratePCR0Manager.s.sol
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
pragma solidity 0.8.28;
|
||||||
|
|
||||||
|
import {Script} from "forge-std/Script.sol";
|
||||||
|
import {console2} from "forge-std/console2.sol";
|
||||||
|
import {PCR0Manager} from "../contracts/utils/PCR0Manager.sol";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @title MigratePCR0Manager
|
||||||
|
* @notice Foundry script to deploy and initialize new PCR0Manager with AccessControl governance
|
||||||
|
*
|
||||||
|
* This script:
|
||||||
|
* 1. Deploys new PCR0Manager (V2 with AccessControl)
|
||||||
|
* 2. Adds all 7 finalized PCR0 values
|
||||||
|
* 3. Transfers roles to multisigs
|
||||||
|
* 4. Deployer renounces all roles
|
||||||
|
* 5. Verifies final state
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* - Set in .env file:
|
||||||
|
* SECURITY_GOVERNANCE_ADDRESS=0x...
|
||||||
|
* OPERATIONS_GOVERNANCE_ADDRESS=0x...
|
||||||
|
* - Dry run: forge script script/MigratePCR0Manager.s.sol --fork-url $CELO_RPC_URL -vvv
|
||||||
|
* - Execute: forge script script/MigratePCR0Manager.s.sol --rpc-url https://forno.celo.org --broadcast --verify -vvv
|
||||||
|
*/
|
||||||
|
contract MigratePCR0Manager is Script {
|
||||||
|
// Governance roles
|
||||||
|
bytes32 public constant SECURITY_ROLE = keccak256("SECURITY_ROLE");
|
||||||
|
bytes32 public constant OPERATIONS_ROLE = keccak256("OPERATIONS_ROLE");
|
||||||
|
|
||||||
|
// Multisig addresses (from environment)
|
||||||
|
address securityMultisig;
|
||||||
|
address operationsMultisig;
|
||||||
|
|
||||||
|
function run() external returns (address newPCR0Manager) {
|
||||||
|
console2.log("================================================================================");
|
||||||
|
console2.log("PCR0MANAGER DEPLOYMENT: Fresh deployment with AccessControl");
|
||||||
|
console2.log("================================================================================");
|
||||||
|
|
||||||
|
console2.log("\nDeployer:", msg.sender);
|
||||||
|
console2.log("Chain ID:", block.chainid);
|
||||||
|
|
||||||
|
// Get multisig addresses from .env
|
||||||
|
securityMultisig = vm.envAddress("SECURITY_GOVERNANCE_ADDRESS");
|
||||||
|
operationsMultisig = vm.envAddress("OPERATIONS_GOVERNANCE_ADDRESS");
|
||||||
|
|
||||||
|
require(securityMultisig != address(0), "SECURITY_GOVERNANCE_ADDRESS not set in .env");
|
||||||
|
require(operationsMultisig != address(0), "OPERATIONS_GOVERNANCE_ADDRESS not set in .env");
|
||||||
|
|
||||||
|
console2.log("\nGovernance addresses:");
|
||||||
|
console2.log(" Critical Multisig:", securityMultisig);
|
||||||
|
console2.log(" Standard Multisig:", operationsMultisig);
|
||||||
|
|
||||||
|
// Get finalized PCR0 values
|
||||||
|
bytes[] memory pcr0Values = getFinalizedPCR0Values();
|
||||||
|
|
||||||
|
console2.log("\nPCR0 values to add:", pcr0Values.length);
|
||||||
|
|
||||||
|
vm.startBroadcast();
|
||||||
|
|
||||||
|
// Step 1: Deploy PCR0Manager
|
||||||
|
console2.log("\n=== Step 1: Deploy PCR0Manager ===");
|
||||||
|
PCR0Manager pcr0Manager = new PCR0Manager();
|
||||||
|
newPCR0Manager = address(pcr0Manager);
|
||||||
|
console2.log("Deployed at:", newPCR0Manager);
|
||||||
|
|
||||||
|
// Step 2: Add PCR0 values
|
||||||
|
console2.log("\n=== Step 2: Add PCR0 Values ===");
|
||||||
|
for (uint256 i = 0; i < pcr0Values.length; i++) {
|
||||||
|
pcr0Manager.addPCR0(pcr0Values[i]);
|
||||||
|
console2.log(" Added PCR0", i + 1, "of", pcr0Values.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Transfer roles to multisigs
|
||||||
|
console2.log("\n=== Step 3: Transfer Roles to Multisigs ===");
|
||||||
|
pcr0Manager.grantRole(SECURITY_ROLE, securityMultisig);
|
||||||
|
pcr0Manager.grantRole(OPERATIONS_ROLE, operationsMultisig);
|
||||||
|
console2.log(" Granted SECURITY_ROLE to:", securityMultisig);
|
||||||
|
console2.log(" Granted OPERATIONS_ROLE to:", operationsMultisig);
|
||||||
|
|
||||||
|
// Step 4: Deployer renounces roles
|
||||||
|
console2.log("\n=== Step 4: Deployer Renounces All Roles ===");
|
||||||
|
pcr0Manager.renounceRole(SECURITY_ROLE, msg.sender);
|
||||||
|
pcr0Manager.renounceRole(OPERATIONS_ROLE, msg.sender);
|
||||||
|
console2.log(" Deployer renounced SECURITY_ROLE");
|
||||||
|
console2.log(" Deployer renounced OPERATIONS_ROLE");
|
||||||
|
|
||||||
|
vm.stopBroadcast();
|
||||||
|
|
||||||
|
// Step 5: Verify final state
|
||||||
|
console2.log("\n=== Step 5: Verify Final State ===");
|
||||||
|
verifyFinalState(pcr0Manager, pcr0Values);
|
||||||
|
|
||||||
|
console2.log("\n================================================================================");
|
||||||
|
console2.log("DEPLOYMENT COMPLETE!");
|
||||||
|
console2.log("================================================================================");
|
||||||
|
console2.log("\nNew PCR0Manager:", newPCR0Manager);
|
||||||
|
console2.log("Total PCR0 values:", pcr0Values.length);
|
||||||
|
console2.log("Governance:");
|
||||||
|
console2.log(" Critical Multisig:", securityMultisig);
|
||||||
|
console2.log(" Standard Multisig:", operationsMultisig);
|
||||||
|
console2.log("\nNext steps:");
|
||||||
|
console2.log("1. Update Hub to point to new PCR0Manager");
|
||||||
|
console2.log("2. Update documentation with new address");
|
||||||
|
console2.log("3. Verify contract on Celoscan");
|
||||||
|
|
||||||
|
return newPCR0Manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Returns finalized PCR0 values (32-byte format)
|
||||||
|
* @dev These will be padded to 48 bytes by PCR0Manager (prefixed with 16 zero bytes)
|
||||||
|
*/
|
||||||
|
function getFinalizedPCR0Values() internal pure returns (bytes[] memory) {
|
||||||
|
bytes[] memory pcr0s = new bytes[](7);
|
||||||
|
|
||||||
|
pcr0s[0] = hex"eb71776987d5f057030823f591d160c9d5d5e0a96c9a2a826778be1da2b8302a";
|
||||||
|
pcr0s[1] = hex"d2221a0ee83901980c607ceff2edbedf3f6ce5f437eafa5d89be39e9e7487c04";
|
||||||
|
pcr0s[2] = hex"4458aeb87796e92700be2d9c2984e376bce42bd80a4bf679e060d3bdaa6de119";
|
||||||
|
pcr0s[3] = hex"aa3deefa408710420e8b4ffe5b95f1dafeb4f06cb16ea44ec7353944671c660a";
|
||||||
|
pcr0s[4] = hex"b31e0df12cd52b961590796511d91a26364dd963c4aa727246b40513e470c232";
|
||||||
|
pcr0s[5] = hex"26bc53c698f78016ad7c326198d25d309d1487098af3f28fc55e951f903e9596";
|
||||||
|
pcr0s[6] = hex"b62720bdb510c2830cf9d58caa23912d0b214d6c278bf22e90942a6b69d272af";
|
||||||
|
|
||||||
|
return pcr0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Verifies the final state of the deployed PCR0Manager
|
||||||
|
*/
|
||||||
|
function verifyFinalState(PCR0Manager pcr0Manager, bytes[] memory pcr0Values) internal view {
|
||||||
|
// Verify all PCR0 values are set (need 48-byte format for checking)
|
||||||
|
for (uint256 i = 0; i < pcr0Values.length; i++) {
|
||||||
|
// Pad to 48 bytes
|
||||||
|
bytes memory padded = abi.encodePacked(new bytes(16), pcr0Values[i]);
|
||||||
|
bool isSet = pcr0Manager.isPCR0Set(padded);
|
||||||
|
require(isSet, "PCR0 value not set");
|
||||||
|
}
|
||||||
|
console2.log(" [PASS] All", pcr0Values.length, "PCR0 values verified");
|
||||||
|
|
||||||
|
// Verify deployer has no roles
|
||||||
|
bool deployerHasCritical = pcr0Manager.hasRole(SECURITY_ROLE, msg.sender);
|
||||||
|
bool deployerHasStandard = pcr0Manager.hasRole(OPERATIONS_ROLE, msg.sender);
|
||||||
|
require(!deployerHasCritical, "Deployer still has SECURITY_ROLE");
|
||||||
|
require(!deployerHasStandard, "Deployer still has OPERATIONS_ROLE");
|
||||||
|
console2.log(" [PASS] Deployer has no roles");
|
||||||
|
|
||||||
|
// Verify multisigs have roles
|
||||||
|
bool criticalHasRole = pcr0Manager.hasRole(SECURITY_ROLE, securityMultisig);
|
||||||
|
bool standardHasRole = pcr0Manager.hasRole(OPERATIONS_ROLE, operationsMultisig);
|
||||||
|
require(criticalHasRole, "Critical multisig missing SECURITY_ROLE");
|
||||||
|
require(standardHasRole, "Standard multisig missing OPERATIONS_ROLE");
|
||||||
|
console2.log(" [PASS] Multisigs have correct roles");
|
||||||
|
|
||||||
|
console2.log("\n [SUCCESS] All verifications passed!");
|
||||||
|
}
|
||||||
|
}
|
||||||
179
contracts/tasks/upgrade/README.md
Normal file
179
contracts/tasks/upgrade/README.md
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
# Upgrade Tooling
|
||||||
|
|
||||||
|
A comprehensive toolset for safely upgrading UUPS proxy contracts in the Self Protocol.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The upgrade tooling provides:
|
||||||
|
|
||||||
|
- **Safety checks** - Storage layout validation, version validation, reinitializer verification
|
||||||
|
- **Safe multisig integration** - Creates proposals for SECURITY_ROLE approval
|
||||||
|
- **Version tracking** - Automatic registry updates and git tagging
|
||||||
|
- **Audit trail** - Complete deployment history with changelogs
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Single command to validate, deploy, and propose
|
||||||
|
npx hardhat upgrade --contract IdentityVerificationHub --network celo --changelog "Added feature X"
|
||||||
|
```
|
||||||
|
|
||||||
|
## The `upgrade` Command
|
||||||
|
|
||||||
|
Validates, deploys, and creates a Safe multisig proposal in one step.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx hardhat upgrade \
|
||||||
|
--contract <ContractId> \
|
||||||
|
--network <network> \
|
||||||
|
[--changelog <message>] \
|
||||||
|
[--prepare-only]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
|
||||||
|
- `--contract` - Contract to upgrade (IdentityVerificationHub, IdentityRegistry, etc.)
|
||||||
|
- `--network` - Target network (celo, sepolia, localhost)
|
||||||
|
- `--changelog` - Description of changes
|
||||||
|
- `--prepare-only` - Deploy implementation without creating Safe proposal
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
|
||||||
|
1. ✅ Validates `@custom:version` increment
|
||||||
|
2. ✅ Checks `reinitializer(N)` matches expected version
|
||||||
|
3. ✅ Validates storage layout compatibility
|
||||||
|
4. ✅ Clears cache and compiles fresh
|
||||||
|
5. ✅ Compares bytecode (warns if unchanged)
|
||||||
|
6. ✅ Deploys new implementation
|
||||||
|
7. ✅ Updates deployment registry
|
||||||
|
8. ✅ Creates git commit and tag
|
||||||
|
9. ✅ Creates Safe proposal (or outputs manual instructions)
|
||||||
|
|
||||||
|
## Utility Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check current deployment status
|
||||||
|
npx hardhat upgrade:status --contract IdentityVerificationHub --network celo
|
||||||
|
|
||||||
|
# View version history
|
||||||
|
npx hardhat upgrade:history --contract IdentityVerificationHub
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
### For Developers
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ 1. UPDATE CONTRACT CODE │
|
||||||
|
│ - Make your changes │
|
||||||
|
│ - Update @custom:version in NatSpec │
|
||||||
|
│ - Increment reinitializer(N) modifier │
|
||||||
|
│ - Add new storage fields at END of struct only │
|
||||||
|
├─────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ 2. RUN: npx hardhat upgrade --contract X --network Y --changelog Z │
|
||||||
|
│ - Validates all safety checks │
|
||||||
|
│ - Deploys new implementation │
|
||||||
|
│ - Updates registry.json │
|
||||||
|
│ - Creates git commit + tag │
|
||||||
|
│ - Creates Safe proposal │
|
||||||
|
├─────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ 3. MULTISIG APPROVAL │
|
||||||
|
│ - Signers review in Safe UI │
|
||||||
|
│ - Once threshold met, click Execute │
|
||||||
|
└─────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Contract Update Pattern
|
||||||
|
|
||||||
|
```solidity
|
||||||
|
/**
|
||||||
|
* @title MyContract
|
||||||
|
* @custom:version 2.13.0 // <-- Update this
|
||||||
|
*/
|
||||||
|
contract MyContract is ImplRoot {
|
||||||
|
|
||||||
|
struct MyStorage {
|
||||||
|
uint256 existingField;
|
||||||
|
uint256 newField; // <-- Add new fields at end only
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment reinitializer(N) for each upgrade
|
||||||
|
function initialize(...) external reinitializer(13) {
|
||||||
|
// Initialize new fields if needed
|
||||||
|
MyStorage storage $ = _getMyStorage();
|
||||||
|
if ($.newField == 0) {
|
||||||
|
$.newField = defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Deployment Registry
|
||||||
|
|
||||||
|
The registry (`deployments/registry.json`) tracks:
|
||||||
|
|
||||||
|
- Proxy addresses per network
|
||||||
|
- Current versions
|
||||||
|
- Implementation history
|
||||||
|
- Git commits and tags
|
||||||
|
|
||||||
|
### Governance Configuration
|
||||||
|
|
||||||
|
Multisig addresses are configured in `deployments/registry.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"networks": {
|
||||||
|
"celo": {
|
||||||
|
"governance": {
|
||||||
|
"securityMultisig": "0x...",
|
||||||
|
"operationsMultisig": "0x...",
|
||||||
|
"securityThreshold": "3/5",
|
||||||
|
"operationsThreshold": "2/5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
Required for deployments:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PRIVATE_KEY=0x... # Deployer private key
|
||||||
|
CELO_RPC_URL=https://... # RPC endpoint
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supported Contracts
|
||||||
|
|
||||||
|
| Contract ID | Contract Name | Type |
|
||||||
|
| ------------------------- | ----------------------------- | ---------- |
|
||||||
|
| `IdentityVerificationHub` | IdentityVerificationHubImplV2 | UUPS Proxy |
|
||||||
|
| `IdentityRegistry` | IdentityRegistryImplV1 | UUPS Proxy |
|
||||||
|
| `IdentityRegistryIdCard` | IdentityRegistryIdCardImplV1 | UUPS Proxy |
|
||||||
|
| `IdentityRegistryAadhaar` | IdentityRegistryAadhaarImplV1 | UUPS Proxy |
|
||||||
|
|
||||||
|
## Safety Checks
|
||||||
|
|
||||||
|
| Check | What it Does | Failure Behavior |
|
||||||
|
| ---------------------- | ------------------------------------------- | -------------------- |
|
||||||
|
| Version validation | Ensures semantic version increment | Blocks upgrade |
|
||||||
|
| Reinitializer check | Verifies `reinitializer(N)` matches version | Blocks upgrade |
|
||||||
|
| Storage layout | Detects breaking storage changes | Blocks upgrade |
|
||||||
|
| Bytecode comparison | Warns if code unchanged | Prompts confirmation |
|
||||||
|
| Safe role verification | Confirms Safe has SECURITY_ROLE | Blocks upgrade |
|
||||||
|
| Constructor check | Flags `_disableInitializers()` | Prompts confirmation |
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Issue | Solution |
|
||||||
|
| ----------------------------- | ----------------------------------------- |
|
||||||
|
| "Version matches current" | Update `@custom:version` in contract |
|
||||||
|
| "Reinitializer mismatch" | Update `reinitializer(N)` to next version |
|
||||||
|
| "Storage layout incompatible" | Don't remove/reorder storage variables |
|
||||||
|
| "Safe not indexed" | Submit manually via Safe UI |
|
||||||
|
| "Bytecode unchanged" | Ensure you saved contract changes |
|
||||||
91
contracts/tasks/upgrade/history.ts
Normal file
91
contracts/tasks/upgrade/history.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
/**
|
||||||
|
* upgrade:history task
|
||||||
|
*
|
||||||
|
* Shows deployment history for a contract.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { task, types } from "hardhat/config";
|
||||||
|
import { log, readRegistry, getContractDefinition, compareVersions, shortenAddress } from "./utils";
|
||||||
|
import { CONTRACT_IDS, ContractId } from "./types";
|
||||||
|
|
||||||
|
interface HistoryTaskArgs {
|
||||||
|
contract: ContractId;
|
||||||
|
}
|
||||||
|
|
||||||
|
task("upgrade:history", "Show deployment history for a contract")
|
||||||
|
.addParam("contract", `Contract to show history for (${CONTRACT_IDS.join(", ")})`, undefined, types.string)
|
||||||
|
.setAction(async (args: HistoryTaskArgs) => {
|
||||||
|
const { contract: contractId } = args;
|
||||||
|
|
||||||
|
log.header(`DEPLOYMENT HISTORY: ${contractId}`);
|
||||||
|
|
||||||
|
if (!CONTRACT_IDS.includes(contractId as ContractId)) {
|
||||||
|
log.error(`Invalid contract: ${contractId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const registry = readRegistry();
|
||||||
|
const contractDef = getContractDefinition(contractId);
|
||||||
|
const versions = registry.versions[contractId] || {};
|
||||||
|
|
||||||
|
// Contract info
|
||||||
|
console.log("\n📋 Contract Information");
|
||||||
|
console.log("─".repeat(60));
|
||||||
|
log.detail("Source", contractDef.source);
|
||||||
|
log.detail("Type", contractDef.type);
|
||||||
|
log.detail("Description", contractDef.description);
|
||||||
|
|
||||||
|
// Network deployments
|
||||||
|
console.log("\n🔗 Network Deployments");
|
||||||
|
console.log("─".repeat(60));
|
||||||
|
|
||||||
|
for (const [networkName, networkConfig] of Object.entries(registry.networks)) {
|
||||||
|
const deployment = networkConfig.deployments[contractId];
|
||||||
|
if (deployment) {
|
||||||
|
const address = deployment.proxy || deployment.address || "Not deployed";
|
||||||
|
const version = deployment.currentVersion || "N/A";
|
||||||
|
console.log(` ${networkName.padEnd(15)} ${shortenAddress(address).padEnd(15)} v${version}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version history
|
||||||
|
console.log("\n📜 Version History");
|
||||||
|
console.log("─".repeat(60));
|
||||||
|
|
||||||
|
const versionNumbers = Object.keys(versions).sort((a, b) => compareVersions(b, a));
|
||||||
|
|
||||||
|
if (versionNumbers.length === 0) {
|
||||||
|
console.log(" No versions recorded");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const version of versionNumbers) {
|
||||||
|
const info = versions[version];
|
||||||
|
const isCurrent = Object.values(registry.networks).some(
|
||||||
|
(n) => n.deployments[contractId]?.currentVersion === version,
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`\n ${isCurrent ? "→" : " "} v${version} (Initializer v${info.initializerVersion})`);
|
||||||
|
if (isCurrent) {
|
||||||
|
console.log(" CURRENT");
|
||||||
|
}
|
||||||
|
console.log(" " + "─".repeat(50));
|
||||||
|
console.log(` Changelog: ${info.changelog}`);
|
||||||
|
console.log(` Initializer: ${info.initializerFunction}()`);
|
||||||
|
console.log(` Git tag: ${info.gitTag}`);
|
||||||
|
|
||||||
|
// Show deployments per network
|
||||||
|
if (info.deployments && Object.keys(info.deployments).length > 0) {
|
||||||
|
console.log(" Deployments:");
|
||||||
|
for (const [network, deployment] of Object.entries(info.deployments)) {
|
||||||
|
if (deployment.impl) {
|
||||||
|
const date = deployment.deployedAt ? new Date(deployment.deployedAt).toLocaleString() : "Unknown";
|
||||||
|
console.log(` ${network}: ${shortenAddress(deployment.impl)} (${date})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("\n");
|
||||||
|
});
|
||||||
|
|
||||||
|
export {};
|
||||||
14
contracts/tasks/upgrade/index.ts
Normal file
14
contracts/tasks/upgrade/index.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* Upgrade Tasks
|
||||||
|
*
|
||||||
|
* Main entry point for contract upgrade tooling.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Import all upgrade tasks
|
||||||
|
import "./upgrade";
|
||||||
|
import "./prepare";
|
||||||
|
import "./propose";
|
||||||
|
import "./status";
|
||||||
|
import "./history";
|
||||||
|
|
||||||
|
export {};
|
||||||
403
contracts/tasks/upgrade/prepare.ts
Normal file
403
contracts/tasks/upgrade/prepare.ts
Normal file
@@ -0,0 +1,403 @@
|
|||||||
|
/**
|
||||||
|
* upgrade:prepare task
|
||||||
|
*
|
||||||
|
* Validates and deploys a new implementation contract.
|
||||||
|
* Does NOT execute the upgrade - that requires multisig approval.
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - Auto-validates version increment (must be current + 1)
|
||||||
|
* - Auto-updates @custom:version in contract if needed
|
||||||
|
* - Auto-commits after successful deployment
|
||||||
|
* - Records git commit hash and creates tag
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* npx hardhat upgrade:prepare --contract IdentityVerificationHub --network celo
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { task, types } from "hardhat/config";
|
||||||
|
import { HardhatRuntimeEnvironment } from "hardhat/types";
|
||||||
|
import {
|
||||||
|
log,
|
||||||
|
getContractDefinition,
|
||||||
|
getProxyAddress,
|
||||||
|
getCurrentVersion,
|
||||||
|
getGitCommitShort,
|
||||||
|
getGitBranch,
|
||||||
|
hasUncommittedChanges,
|
||||||
|
validateVersionIncrement,
|
||||||
|
suggestNextVersion,
|
||||||
|
readContractVersion,
|
||||||
|
updateContractVersion,
|
||||||
|
getContractFilePath,
|
||||||
|
addVersion,
|
||||||
|
getExplorerUrl,
|
||||||
|
shortenAddress,
|
||||||
|
createGitTag,
|
||||||
|
gitCommit,
|
||||||
|
getLatestVersionInfo,
|
||||||
|
} from "./utils";
|
||||||
|
import { CONTRACT_IDS, ContractId, SupportedNetwork } from "./types";
|
||||||
|
|
||||||
|
interface PrepareTaskArgs {
|
||||||
|
contract: ContractId;
|
||||||
|
newVersion?: string;
|
||||||
|
changelog?: string;
|
||||||
|
dryRun: boolean;
|
||||||
|
skipCommit: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
task("upgrade:prepare", "Validate and deploy a new implementation contract")
|
||||||
|
.addParam("contract", `Contract to upgrade (${CONTRACT_IDS.join(", ")})`, undefined, types.string)
|
||||||
|
.addOptionalParam(
|
||||||
|
"newVersion",
|
||||||
|
"New version - auto-detected from contract file if not provided",
|
||||||
|
undefined,
|
||||||
|
types.string,
|
||||||
|
)
|
||||||
|
.addOptionalParam("changelog", "Changelog entry for this version", undefined, types.string)
|
||||||
|
.addFlag("dryRun", "Simulate the deployment without actually deploying")
|
||||||
|
.addFlag("skipCommit", "Skip auto-commit after deployment")
|
||||||
|
.setAction(async (args: PrepareTaskArgs, hre: HardhatRuntimeEnvironment) => {
|
||||||
|
const { contract: contractId, changelog, dryRun, skipCommit } = args;
|
||||||
|
let { newVersion } = args;
|
||||||
|
const network = hre.network.name as SupportedNetwork;
|
||||||
|
|
||||||
|
log.header(`UPGRADE PREPARE: ${contractId}`);
|
||||||
|
log.detail("Network", network);
|
||||||
|
log.detail("Mode", dryRun ? "DRY RUN (no actual deployment)" : "LIVE DEPLOYMENT");
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Step 1: Validate inputs
|
||||||
|
// ========================================================================
|
||||||
|
log.step("Validating inputs...");
|
||||||
|
|
||||||
|
if (!CONTRACT_IDS.includes(contractId as ContractId)) {
|
||||||
|
log.error(`Invalid contract: ${contractId}`);
|
||||||
|
log.info(`Valid contracts: ${CONTRACT_IDS.join(", ")}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const contractDef = getContractDefinition(contractId);
|
||||||
|
if (contractDef.type !== "uups-proxy") {
|
||||||
|
log.error(`Contract '${contractId}' is not upgradeable (type: ${contractDef.type})`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let proxyAddress: string;
|
||||||
|
try {
|
||||||
|
proxyAddress = getProxyAddress(contractId, network);
|
||||||
|
} catch {
|
||||||
|
log.error(`No proxy deployed for '${contractId}' on network '${network}'`);
|
||||||
|
log.info("Deploy the proxy first using the deploy script");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentVersion = getCurrentVersion(contractId, network);
|
||||||
|
|
||||||
|
log.detail("Contract source", contractDef.source);
|
||||||
|
log.detail("Proxy address", proxyAddress);
|
||||||
|
log.detail("Current version", currentVersion);
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Step 2: Determine and validate new version
|
||||||
|
// ========================================================================
|
||||||
|
log.step("Validating version...");
|
||||||
|
|
||||||
|
const contractFilePath = getContractFilePath(contractId);
|
||||||
|
const contractFileVersion = contractFilePath ? readContractVersion(contractFilePath) : null;
|
||||||
|
|
||||||
|
if (contractFileVersion) {
|
||||||
|
log.detail("Version in contract file", contractFileVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no version provided, use contract file version
|
||||||
|
if (!newVersion) {
|
||||||
|
if (contractFileVersion && contractFileVersion !== currentVersion) {
|
||||||
|
const validation = validateVersionIncrement(currentVersion, contractFileVersion);
|
||||||
|
if (validation.valid) {
|
||||||
|
newVersion = contractFileVersion;
|
||||||
|
log.info(`Using version from contract file: ${newVersion}`);
|
||||||
|
} else {
|
||||||
|
log.error(`Contract file has invalid version ${contractFileVersion}`);
|
||||||
|
const suggestions = suggestNextVersion(currentVersion);
|
||||||
|
log.info(`Current version: ${currentVersion}`);
|
||||||
|
log.info(
|
||||||
|
`Valid next versions: ${suggestions.patch} (patch), ${suggestions.minor} (minor), ${suggestions.major} (major)`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.error("Contract file version matches current - update @custom:version in contract first");
|
||||||
|
const suggestions = suggestNextVersion(currentVersion);
|
||||||
|
log.info(`Current version: ${currentVersion}`);
|
||||||
|
log.info(
|
||||||
|
`Valid next versions: ${suggestions.patch} (patch), ${suggestions.minor} (minor), ${suggestions.major} (major)`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate version increment
|
||||||
|
const versionValidation = validateVersionIncrement(currentVersion, newVersion);
|
||||||
|
if (!versionValidation.valid) {
|
||||||
|
log.error(versionValidation.error!);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.success(`Version increment valid: ${currentVersion} → ${newVersion} (${versionValidation.type})`);
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Step 3: Update contract file version if needed
|
||||||
|
// ========================================================================
|
||||||
|
if (contractFilePath && contractFileVersion !== newVersion) {
|
||||||
|
log.step("Updating contract file version...");
|
||||||
|
|
||||||
|
if (dryRun) {
|
||||||
|
log.info(`[DRY RUN] Would update ${contractFilePath} to version ${newVersion}`);
|
||||||
|
} else {
|
||||||
|
const updated = updateContractVersion(contractFilePath, newVersion);
|
||||||
|
if (updated) {
|
||||||
|
log.success(`Updated @custom:version to ${newVersion} in contract file`);
|
||||||
|
} else {
|
||||||
|
log.warning("Could not update version in contract file - please update manually");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Step 4: Check git state
|
||||||
|
// ========================================================================
|
||||||
|
log.step("Checking git state...");
|
||||||
|
|
||||||
|
const gitBranch = getGitBranch();
|
||||||
|
const uncommittedChanges = hasUncommittedChanges();
|
||||||
|
|
||||||
|
log.detail("Branch", gitBranch);
|
||||||
|
|
||||||
|
if (uncommittedChanges && !dryRun) {
|
||||||
|
log.warning("You have uncommitted changes. They will be included in the auto-commit.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Step 5: Load and validate the new implementation
|
||||||
|
// ========================================================================
|
||||||
|
log.step("Loading contract factory...");
|
||||||
|
|
||||||
|
const contractName = contractDef.source;
|
||||||
|
let ContractFactory;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Handle contracts that need library linking
|
||||||
|
if (contractName === "IdentityVerificationHubImplV2") {
|
||||||
|
const CustomVerifier = await hre.ethers.getContractFactory("CustomVerifier");
|
||||||
|
const customVerifier = await CustomVerifier.deploy();
|
||||||
|
await customVerifier.waitForDeployment();
|
||||||
|
|
||||||
|
ContractFactory = await hre.ethers.getContractFactory(contractName, {
|
||||||
|
libraries: {
|
||||||
|
CustomVerifier: await customVerifier.getAddress(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
log.info("Deployed CustomVerifier library for linking");
|
||||||
|
} else if (
|
||||||
|
contractName === "IdentityRegistryImplV1" ||
|
||||||
|
contractName === "IdentityRegistryIdCardImplV1" ||
|
||||||
|
contractName === "IdentityRegistryAadhaarImplV1"
|
||||||
|
) {
|
||||||
|
const PoseidonT3 = await hre.ethers.getContractFactory("PoseidonT3");
|
||||||
|
const poseidonT3 = await PoseidonT3.deploy();
|
||||||
|
await poseidonT3.waitForDeployment();
|
||||||
|
|
||||||
|
ContractFactory = await hre.ethers.getContractFactory(contractName, {
|
||||||
|
libraries: {
|
||||||
|
PoseidonT3: await poseidonT3.getAddress(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
log.info("Deployed PoseidonT3 library for linking");
|
||||||
|
} else {
|
||||||
|
ContractFactory = await hre.ethers.getContractFactory(contractName);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.success(`Loaded contract factory: ${contractName}`);
|
||||||
|
} catch (error) {
|
||||||
|
log.error(`Failed to load contract factory: ${error}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Step 6: Validate storage layout
|
||||||
|
// ========================================================================
|
||||||
|
log.step("Validating storage layout compatibility...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await hre.upgrades.validateImplementation(ContractFactory, {
|
||||||
|
kind: "uups",
|
||||||
|
unsafeAllowLinkedLibraries: true,
|
||||||
|
unsafeAllow: ["constructor", "external-library-linking"],
|
||||||
|
});
|
||||||
|
log.success("Storage layout validation passed");
|
||||||
|
} catch (error) {
|
||||||
|
log.error(`Storage layout validation failed: ${error}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Step 7: Simulate upgrade on fork
|
||||||
|
// ========================================================================
|
||||||
|
log.step("Simulating upgrade on fork...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const proxyContract = await hre.ethers.getContractAt(contractName, proxyAddress);
|
||||||
|
const SECURITY_ROLE = await proxyContract.SECURITY_ROLE();
|
||||||
|
log.detail("SECURITY_ROLE", SECURITY_ROLE);
|
||||||
|
log.success("Fork simulation passed - proxy is accessible");
|
||||||
|
} catch (error) {
|
||||||
|
log.error(`Fork simulation failed: ${error}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Step 8: Dry run summary
|
||||||
|
// ========================================================================
|
||||||
|
if (dryRun) {
|
||||||
|
log.step("DRY RUN - Skipping actual deployment");
|
||||||
|
log.box([
|
||||||
|
"DRY RUN SUMMARY",
|
||||||
|
"─".repeat(50),
|
||||||
|
`Contract: ${contractId}`,
|
||||||
|
`Version: ${currentVersion} → ${newVersion}`,
|
||||||
|
`Network: ${network}`,
|
||||||
|
`Proxy: ${proxyAddress}`,
|
||||||
|
"",
|
||||||
|
"What would happen:",
|
||||||
|
`1. Update contract file to version ${newVersion}`,
|
||||||
|
"2. Deploy new implementation",
|
||||||
|
"3. Update registry.json",
|
||||||
|
"4. Create git commit and tag",
|
||||||
|
"",
|
||||||
|
"Run without --dry-run to execute.",
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Step 9: Deploy new implementation
|
||||||
|
// ========================================================================
|
||||||
|
log.step("Deploying new implementation...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const implementation = await ContractFactory.deploy();
|
||||||
|
await implementation.waitForDeployment();
|
||||||
|
const implementationAddress = await implementation.getAddress();
|
||||||
|
|
||||||
|
log.success(`Implementation deployed: ${implementationAddress}`);
|
||||||
|
log.detail("Explorer", `${getExplorerUrl(network)}/address/${implementationAddress}`);
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Step 10: Verify on block explorer
|
||||||
|
// ========================================================================
|
||||||
|
log.step("Verifying contract on block explorer...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await hre.run("verify:verify", {
|
||||||
|
address: implementationAddress,
|
||||||
|
constructorArguments: [],
|
||||||
|
});
|
||||||
|
log.success("Contract verified on block explorer");
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.message?.includes("Already Verified")) {
|
||||||
|
log.info("Contract already verified");
|
||||||
|
} else {
|
||||||
|
log.warning(`Verification failed: ${error.message}`);
|
||||||
|
log.info("You can verify manually later");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Step 11: Update registry
|
||||||
|
// ========================================================================
|
||||||
|
log.step("Updating deployment registry...");
|
||||||
|
|
||||||
|
// Get previous version info to determine initializer version
|
||||||
|
const latestVersion = getLatestVersionInfo(contractId);
|
||||||
|
const newInitializerVersion = (latestVersion?.info.initializerVersion || 0) + 1;
|
||||||
|
|
||||||
|
const gitCommitShort = getGitCommitShort();
|
||||||
|
const deployerAddress = (await hre.ethers.provider.getSigner()).address;
|
||||||
|
|
||||||
|
addVersion(
|
||||||
|
contractId,
|
||||||
|
network,
|
||||||
|
newVersion,
|
||||||
|
{
|
||||||
|
initializerVersion: newInitializerVersion,
|
||||||
|
initializerFunction: newInitializerVersion === 1 ? "initialize" : `initializeV${newInitializerVersion}`,
|
||||||
|
changelog: changelog || `Upgrade to v${newVersion}`,
|
||||||
|
gitTag: `${contractId.toLowerCase()}-v${newVersion}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
impl: implementationAddress,
|
||||||
|
deployedAt: new Date().toISOString(),
|
||||||
|
deployedBy: deployerAddress,
|
||||||
|
gitCommit: "", // Will be set after commit
|
||||||
|
},
|
||||||
|
);
|
||||||
|
log.success("Registry updated");
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Step 12: Auto-commit and tag
|
||||||
|
// ========================================================================
|
||||||
|
if (!skipCommit) {
|
||||||
|
log.step("Creating git commit...");
|
||||||
|
|
||||||
|
const commitMessage = `feat: ${contractId} v${newVersion} deployed on ${network.charAt(0).toUpperCase() + network.slice(1)}
|
||||||
|
|
||||||
|
- Implementation: ${implementationAddress}
|
||||||
|
- Changelog: ${changelog || "Upgrade"}`;
|
||||||
|
|
||||||
|
const committed = gitCommit(commitMessage);
|
||||||
|
if (committed) {
|
||||||
|
const newGitCommit = getGitCommitShort();
|
||||||
|
log.success(`Committed: ${newGitCommit}`);
|
||||||
|
|
||||||
|
// Try to create git tag
|
||||||
|
try {
|
||||||
|
createGitTag(
|
||||||
|
`${contractId.toLowerCase()}-v${newVersion}`,
|
||||||
|
`${contractId} v${newVersion} - ${changelog || "Upgrade"}`,
|
||||||
|
);
|
||||||
|
log.success(`Created git tag: ${contractId.toLowerCase()}-v${newVersion}`);
|
||||||
|
} catch (e) {
|
||||||
|
log.warning("Could not create git tag - you can create it manually");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warning("Could not create git commit - please commit manually");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Summary
|
||||||
|
// ========================================================================
|
||||||
|
log.box([
|
||||||
|
"DEPLOYMENT SUCCESSFUL",
|
||||||
|
"═".repeat(50),
|
||||||
|
`Contract: ${contractId}`,
|
||||||
|
`Version: ${currentVersion} → ${newVersion}`,
|
||||||
|
`Network: ${network}`,
|
||||||
|
"",
|
||||||
|
"Addresses:",
|
||||||
|
` Proxy: ${shortenAddress(proxyAddress)}`,
|
||||||
|
` New Impl: ${shortenAddress(implementationAddress)}`,
|
||||||
|
"",
|
||||||
|
"Next steps:",
|
||||||
|
` 1. Run: npx hardhat upgrade:propose --contract ${contractId} --network ${network}`,
|
||||||
|
" 2. Multisig signers approve in Safe UI",
|
||||||
|
" 3. Transaction executes automatically when threshold reached",
|
||||||
|
]);
|
||||||
|
} catch (error) {
|
||||||
|
log.error(`Deployment failed: ${error}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export {};
|
||||||
201
contracts/tasks/upgrade/propose.ts
Normal file
201
contracts/tasks/upgrade/propose.ts
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
/**
|
||||||
|
* upgrade:propose task
|
||||||
|
*
|
||||||
|
* Creates a Safe multisig transaction to execute the upgrade.
|
||||||
|
* The implementation must already be deployed via upgrade:prepare.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* npx hardhat upgrade:propose --contract IdentityVerificationHub --network celo
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { task, types } from "hardhat/config";
|
||||||
|
import { HardhatRuntimeEnvironment } from "hardhat/types";
|
||||||
|
import {
|
||||||
|
log,
|
||||||
|
getContractDefinition,
|
||||||
|
getProxyAddress,
|
||||||
|
getCurrentVersion,
|
||||||
|
getGovernanceConfig,
|
||||||
|
getVersionInfo,
|
||||||
|
getNetworkDeployment,
|
||||||
|
} from "./utils";
|
||||||
|
import { CONTRACT_IDS, ContractId, SupportedNetwork } from "./types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Safe chain prefix for URL
|
||||||
|
*/
|
||||||
|
function getChainPrefix(network: SupportedNetwork): string {
|
||||||
|
const prefixes: Record<SupportedNetwork, string> = {
|
||||||
|
celo: "celo",
|
||||||
|
"celo-sepolia": "celo",
|
||||||
|
sepolia: "sep",
|
||||||
|
localhost: "eth",
|
||||||
|
};
|
||||||
|
return prefixes[network] || network;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProposeTaskArgs {
|
||||||
|
contract: ContractId;
|
||||||
|
dryRun: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
task("upgrade:propose", "Create Safe transaction to execute the upgrade")
|
||||||
|
.addParam("contract", `Contract to upgrade (${CONTRACT_IDS.join(", ")})`, undefined, types.string)
|
||||||
|
.addFlag("dryRun", "Generate transaction data without submitting to Safe")
|
||||||
|
.setAction(async (args: ProposeTaskArgs, hre: HardhatRuntimeEnvironment) => {
|
||||||
|
const { contract: contractId, dryRun } = args;
|
||||||
|
const network = hre.network.name as SupportedNetwork;
|
||||||
|
|
||||||
|
log.header(`UPGRADE PROPOSE: ${contractId}`);
|
||||||
|
log.detail("Network", network);
|
||||||
|
log.detail("Mode", dryRun ? "DRY RUN (no Safe submission)" : "LIVE SUBMISSION");
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Step 1: Validate inputs
|
||||||
|
// ========================================================================
|
||||||
|
log.step("Validating inputs...");
|
||||||
|
|
||||||
|
if (!CONTRACT_IDS.includes(contractId as ContractId)) {
|
||||||
|
log.error(`Invalid contract: ${contractId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const contractDef = getContractDefinition(contractId);
|
||||||
|
if (contractDef.type !== "uups-proxy") {
|
||||||
|
log.error(`Contract '${contractId}' is not upgradeable`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let proxyAddress: string;
|
||||||
|
try {
|
||||||
|
proxyAddress = getProxyAddress(contractId, network);
|
||||||
|
} catch {
|
||||||
|
log.error(`No proxy deployed for '${contractId}' on '${network}'`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentVersion = getCurrentVersion(contractId, network);
|
||||||
|
const deployment = getNetworkDeployment(contractId, network);
|
||||||
|
const newImplAddress = deployment?.currentImpl;
|
||||||
|
|
||||||
|
if (!newImplAddress) {
|
||||||
|
log.error("No implementation deployed. Run upgrade:prepare first.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const versionInfo = getVersionInfo(contractId, currentVersion);
|
||||||
|
|
||||||
|
log.detail("Contract", contractId);
|
||||||
|
log.detail("Proxy", proxyAddress);
|
||||||
|
log.detail("Current version", currentVersion);
|
||||||
|
log.detail("New implementation", newImplAddress);
|
||||||
|
log.detail("Changelog", versionInfo?.changelog || "N/A");
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Step 2: Load governance configuration
|
||||||
|
// ========================================================================
|
||||||
|
log.step("Loading governance configuration...");
|
||||||
|
|
||||||
|
const governance = getGovernanceConfig(network);
|
||||||
|
if (!governance.securityMultisig) {
|
||||||
|
log.error(`No security multisig configured for network '${network}'`);
|
||||||
|
log.info("Update deployments/registry.json with governance addresses");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.detail("Security multisig", governance.securityMultisig);
|
||||||
|
log.detail("Required threshold", governance.securityThreshold);
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Step 3: Verify implementation contract
|
||||||
|
// ========================================================================
|
||||||
|
log.step("Verifying implementation contract...");
|
||||||
|
|
||||||
|
const implCode = await hre.ethers.provider.getCode(newImplAddress);
|
||||||
|
if (implCode === "0x") {
|
||||||
|
log.error(`No contract found at implementation address ${newImplAddress}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log.success("Implementation contract verified on-chain");
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Step 4: Build upgrade transaction
|
||||||
|
// ========================================================================
|
||||||
|
log.step("Building upgrade transaction...");
|
||||||
|
|
||||||
|
const contractName = contractDef.source;
|
||||||
|
const proxyContract = await hre.ethers.getContractAt(contractName, proxyAddress);
|
||||||
|
|
||||||
|
// Check if there's an initializer to call
|
||||||
|
let initData = "0x";
|
||||||
|
const initializerName = versionInfo?.initializerFunction;
|
||||||
|
if (initializerName && initializerName !== "initialize") {
|
||||||
|
try {
|
||||||
|
const iface = proxyContract.interface;
|
||||||
|
const initFragment = iface.getFunction(initializerName);
|
||||||
|
if (initFragment) {
|
||||||
|
initData = iface.encodeFunctionData(initializerName, []);
|
||||||
|
log.detail("Initialization", initializerName);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
log.detail("Initialization", "None (function not found)");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.detail("Initialization", "None");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode upgradeToAndCall
|
||||||
|
const upgradeData = proxyContract.interface.encodeFunctionData("upgradeToAndCall", [newImplAddress, initData]);
|
||||||
|
|
||||||
|
log.detail("Method", "upgradeToAndCall(address,bytes)");
|
||||||
|
log.detail("Target", proxyAddress);
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Step 5: Output transaction data
|
||||||
|
// ========================================================================
|
||||||
|
if (dryRun) {
|
||||||
|
log.step("DRY RUN - Transaction data generated");
|
||||||
|
} else {
|
||||||
|
log.step("Generating Safe proposal data...");
|
||||||
|
}
|
||||||
|
|
||||||
|
const chainPrefix = getChainPrefix(network);
|
||||||
|
|
||||||
|
log.success("Transaction data generated");
|
||||||
|
|
||||||
|
log.box([
|
||||||
|
"SAFE PROPOSAL READY",
|
||||||
|
"═".repeat(60),
|
||||||
|
`Safe: ${governance.securityMultisig}`,
|
||||||
|
`Threshold: ${governance.securityThreshold}`,
|
||||||
|
"",
|
||||||
|
"Transaction:",
|
||||||
|
` To: ${proxyAddress}`,
|
||||||
|
" Value: 0",
|
||||||
|
` Data: ${upgradeData.slice(0, 50)}...`,
|
||||||
|
"",
|
||||||
|
"Submit via Safe UI:",
|
||||||
|
` 1. Go to: https://app.safe.global/home?safe=${chainPrefix}:${governance.securityMultisig}`,
|
||||||
|
" 2. Click 'New transaction' → 'Transaction Builder'",
|
||||||
|
" 3. Enter: To, Value, Data from above",
|
||||||
|
` 4. ${governance.securityThreshold} signers approve`,
|
||||||
|
" 5. Upgrade executes automatically!",
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Output raw data for copy-paste
|
||||||
|
console.log("\n📋 Raw transaction data (copy this for Transaction Builder):");
|
||||||
|
console.log("─".repeat(60));
|
||||||
|
console.log(
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
to: proxyAddress,
|
||||||
|
value: "0",
|
||||||
|
data: upgradeData,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export {};
|
||||||
113
contracts/tasks/upgrade/status.ts
Normal file
113
contracts/tasks/upgrade/status.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
/**
|
||||||
|
* upgrade:status task
|
||||||
|
*
|
||||||
|
* Shows current deployment status for a contract.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { task, types } from "hardhat/config";
|
||||||
|
import { HardhatRuntimeEnvironment } from "hardhat/types";
|
||||||
|
import {
|
||||||
|
log,
|
||||||
|
getContractDefinition,
|
||||||
|
getNetworkDeployment,
|
||||||
|
getGovernanceConfig,
|
||||||
|
getVersionInfo,
|
||||||
|
shortenAddress,
|
||||||
|
getExplorerUrl,
|
||||||
|
} from "./utils";
|
||||||
|
import { CONTRACT_IDS, ContractId, SupportedNetwork } from "./types";
|
||||||
|
|
||||||
|
interface StatusTaskArgs {
|
||||||
|
contract: ContractId;
|
||||||
|
}
|
||||||
|
|
||||||
|
task("upgrade:status", "Show current deployment status for a contract")
|
||||||
|
.addParam("contract", `Contract to check (${CONTRACT_IDS.join(", ")})`, undefined, types.string)
|
||||||
|
.setAction(async (args: StatusTaskArgs, hre: HardhatRuntimeEnvironment) => {
|
||||||
|
const { contract: contractId } = args;
|
||||||
|
const network = hre.network.name as SupportedNetwork;
|
||||||
|
|
||||||
|
log.header(`STATUS: ${contractId} on ${network}`);
|
||||||
|
|
||||||
|
if (!CONTRACT_IDS.includes(contractId as ContractId)) {
|
||||||
|
log.error(`Invalid contract: ${contractId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const contractDef = getContractDefinition(contractId);
|
||||||
|
const deployment = getNetworkDeployment(contractId, network);
|
||||||
|
const governance = getGovernanceConfig(network);
|
||||||
|
|
||||||
|
console.log("\n📋 Contract Info");
|
||||||
|
console.log("─".repeat(60));
|
||||||
|
log.detail("Source", contractDef.source);
|
||||||
|
log.detail("Type", contractDef.type);
|
||||||
|
|
||||||
|
if (!deployment) {
|
||||||
|
log.warning(`Not deployed on ${network}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentVersion = deployment.currentVersion;
|
||||||
|
const proxyAddress = deployment.proxy || deployment.address;
|
||||||
|
const implAddress = deployment.currentImpl;
|
||||||
|
const versionInfo = currentVersion ? getVersionInfo(contractId, currentVersion) : null;
|
||||||
|
|
||||||
|
console.log("\n🔗 Deployment");
|
||||||
|
console.log("─".repeat(60));
|
||||||
|
log.detail("Proxy", proxyAddress || "N/A");
|
||||||
|
log.detail("Implementation", implAddress || "N/A");
|
||||||
|
log.detail("Current version", currentVersion || "N/A");
|
||||||
|
if (proxyAddress) {
|
||||||
|
log.detail("Explorer", `${getExplorerUrl(network)}/address/${proxyAddress}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (versionInfo) {
|
||||||
|
console.log("\n📌 Version Info");
|
||||||
|
console.log("─".repeat(60));
|
||||||
|
log.detail("Changelog", versionInfo.changelog);
|
||||||
|
log.detail("Initializer", versionInfo.initializerFunction);
|
||||||
|
log.detail("Git tag", versionInfo.gitTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("\n🔐 Governance");
|
||||||
|
console.log("─".repeat(60));
|
||||||
|
log.detail("Security multisig", governance.securityMultisig || "Not configured");
|
||||||
|
log.detail("Security threshold", governance.securityThreshold);
|
||||||
|
log.detail("Operations multisig", governance.operationsMultisig || "Not configured");
|
||||||
|
log.detail("Operations threshold", governance.operationsThreshold);
|
||||||
|
|
||||||
|
// Check on-chain state if connected
|
||||||
|
if (proxyAddress && contractDef.type === "uups-proxy") {
|
||||||
|
try {
|
||||||
|
const proxy = await hre.ethers.getContractAt(contractDef.source, proxyAddress);
|
||||||
|
|
||||||
|
console.log("\n⛓️ On-Chain State");
|
||||||
|
console.log("─".repeat(60));
|
||||||
|
|
||||||
|
// Try to read version
|
||||||
|
try {
|
||||||
|
const onChainVersion = await proxy.version();
|
||||||
|
log.detail("On-chain version", onChainVersion);
|
||||||
|
} catch {
|
||||||
|
log.detail("On-chain version", "N/A");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check role holders
|
||||||
|
try {
|
||||||
|
const SECURITY_ROLE = await proxy.SECURITY_ROLE();
|
||||||
|
const OPERATIONS_ROLE = await proxy.OPERATIONS_ROLE();
|
||||||
|
log.detail("SECURITY_ROLE", shortenAddress(SECURITY_ROLE));
|
||||||
|
log.detail("OPERATIONS_ROLE", shortenAddress(OPERATIONS_ROLE));
|
||||||
|
} catch {
|
||||||
|
// Not all contracts have these
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log.warning(`Could not read on-chain state: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("\n");
|
||||||
|
});
|
||||||
|
|
||||||
|
export {};
|
||||||
29
contracts/tasks/upgrade/types.ts
Normal file
29
contracts/tasks/upgrade/types.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* Types for upgrade tooling
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const SUPPORTED_NETWORKS = ["celo", "celo-sepolia", "sepolia", "localhost"] as const;
|
||||||
|
export type SupportedNetwork = (typeof SUPPORTED_NETWORKS)[number];
|
||||||
|
|
||||||
|
// Contract IDs match registry keys
|
||||||
|
export const CONTRACT_IDS = [
|
||||||
|
"IdentityVerificationHub",
|
||||||
|
"IdentityRegistry",
|
||||||
|
"IdentityRegistryIdCard",
|
||||||
|
"IdentityRegistryAadhaar",
|
||||||
|
"PCR0Manager",
|
||||||
|
"VerifyAll",
|
||||||
|
"DummyContract",
|
||||||
|
] as const;
|
||||||
|
export type ContractId = (typeof CONTRACT_IDS)[number];
|
||||||
|
|
||||||
|
// Re-export types from utils for convenience
|
||||||
|
export type {
|
||||||
|
ContractDefinition,
|
||||||
|
NetworkConfig,
|
||||||
|
NetworkDeployment,
|
||||||
|
GovernanceConfig,
|
||||||
|
VersionInfo,
|
||||||
|
VersionDeployment,
|
||||||
|
DeploymentRegistry,
|
||||||
|
} from "./utils";
|
||||||
1029
contracts/tasks/upgrade/upgrade.ts
Normal file
1029
contracts/tasks/upgrade/upgrade.ts
Normal file
File diff suppressed because it is too large
Load Diff
631
contracts/tasks/upgrade/utils.ts
Normal file
631
contracts/tasks/upgrade/utils.ts
Normal file
@@ -0,0 +1,631 @@
|
|||||||
|
/**
|
||||||
|
* Utility functions for upgrade tooling
|
||||||
|
*
|
||||||
|
* Works with the new registry structure:
|
||||||
|
* - contracts: Contract definitions (source, type, description)
|
||||||
|
* - networks: Per-network deployments and governance
|
||||||
|
* - versions: Version history with deployment details
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as fs from "fs";
|
||||||
|
import * as path from "path";
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
import { SupportedNetwork } from "./types";
|
||||||
|
|
||||||
|
// Registry types matching the new structure
|
||||||
|
export interface ContractDefinition {
|
||||||
|
source: string;
|
||||||
|
type: "uups-proxy" | "non-upgradeable";
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NetworkDeployment {
|
||||||
|
proxy?: string;
|
||||||
|
address?: string;
|
||||||
|
currentVersion: string;
|
||||||
|
currentImpl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GovernanceConfig {
|
||||||
|
securityMultisig: string;
|
||||||
|
operationsMultisig: string;
|
||||||
|
securityThreshold: string;
|
||||||
|
operationsThreshold: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NetworkConfig {
|
||||||
|
chainId: number;
|
||||||
|
governance: GovernanceConfig;
|
||||||
|
deployments: Record<string, NetworkDeployment>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VersionDeployment {
|
||||||
|
impl: string;
|
||||||
|
deployedAt: string;
|
||||||
|
deployedBy: string;
|
||||||
|
gitCommit: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VersionInfo {
|
||||||
|
initializerVersion: number;
|
||||||
|
initializerFunction: string;
|
||||||
|
changelog: string;
|
||||||
|
gitTag: string;
|
||||||
|
deployments: Record<string, VersionDeployment>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeploymentRegistry {
|
||||||
|
$schema: string;
|
||||||
|
lastUpdated: string;
|
||||||
|
contracts: Record<string, ContractDefinition>;
|
||||||
|
networks: Record<string, NetworkConfig>;
|
||||||
|
versions: Record<string, Record<string, VersionInfo>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Console colors for pretty output
|
||||||
|
const colors = {
|
||||||
|
reset: "\x1b[0m",
|
||||||
|
bold: "\x1b[1m",
|
||||||
|
red: "\x1b[31m",
|
||||||
|
green: "\x1b[32m",
|
||||||
|
yellow: "\x1b[33m",
|
||||||
|
blue: "\x1b[34m",
|
||||||
|
magenta: "\x1b[35m",
|
||||||
|
cyan: "\x1b[36m",
|
||||||
|
white: "\x1b[37m",
|
||||||
|
gray: "\x1b[90m",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const log = {
|
||||||
|
info: (msg: string) => console.log(`${colors.blue}ℹ${colors.reset} ${msg}`),
|
||||||
|
success: (msg: string) => console.log(`${colors.green}✅${colors.reset} ${msg}`),
|
||||||
|
warning: (msg: string) => console.log(`${colors.yellow}⚠️${colors.reset} ${msg}`),
|
||||||
|
error: (msg: string) => console.log(`${colors.red}❌${colors.reset} ${msg}`),
|
||||||
|
step: (msg: string) => console.log(`\n${colors.magenta}🔄${colors.reset} ${colors.bold}${msg}${colors.reset}`),
|
||||||
|
header: (msg: string) =>
|
||||||
|
console.log(
|
||||||
|
`\n${colors.cyan}${"═".repeat(70)}${colors.reset}\n${colors.bold}${msg}${colors.reset}\n${colors.cyan}${"═".repeat(70)}${colors.reset}`,
|
||||||
|
),
|
||||||
|
detail: (label: string, value: string) => console.log(` ${colors.gray}${label}:${colors.reset} ${value}`),
|
||||||
|
box: (lines: string[]) => {
|
||||||
|
const maxLen = Math.max(...lines.map((l) => l.length));
|
||||||
|
console.log(`\n┌${"─".repeat(maxLen + 2)}┐`);
|
||||||
|
lines.forEach((line) => console.log(`│ ${line.padEnd(maxLen)} │`));
|
||||||
|
console.log(`└${"─".repeat(maxLen + 2)}┘\n`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the path to the deployment registry
|
||||||
|
*/
|
||||||
|
export function getRegistryPath(): string {
|
||||||
|
return path.join(__dirname, "../../deployments/registry.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the deployment registry
|
||||||
|
*/
|
||||||
|
export function readRegistry(): DeploymentRegistry {
|
||||||
|
const registryPath = getRegistryPath();
|
||||||
|
if (!fs.existsSync(registryPath)) {
|
||||||
|
throw new Error(`Deployment registry not found at ${registryPath}`);
|
||||||
|
}
|
||||||
|
return JSON.parse(fs.readFileSync(registryPath, "utf-8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write the deployment registry
|
||||||
|
*/
|
||||||
|
export function writeRegistry(registry: DeploymentRegistry): void {
|
||||||
|
const registryPath = getRegistryPath();
|
||||||
|
registry.lastUpdated = new Date().toISOString();
|
||||||
|
fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2) + "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get contract definition from registry
|
||||||
|
*/
|
||||||
|
export function getContractDefinition(contractId: string): ContractDefinition {
|
||||||
|
const registry = readRegistry();
|
||||||
|
const contract = registry.contracts[contractId];
|
||||||
|
if (!contract) {
|
||||||
|
throw new Error(`Contract '${contractId}' not found in registry`);
|
||||||
|
}
|
||||||
|
return contract;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get network config
|
||||||
|
*/
|
||||||
|
export function getNetworkConfig(network: SupportedNetwork): NetworkConfig {
|
||||||
|
const registry = readRegistry();
|
||||||
|
const networkConfig = registry.networks[network];
|
||||||
|
if (!networkConfig) {
|
||||||
|
throw new Error(`Network '${network}' not configured in registry`);
|
||||||
|
}
|
||||||
|
return networkConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get deployment for a contract on a network
|
||||||
|
*/
|
||||||
|
export function getNetworkDeployment(contractId: string, network: SupportedNetwork): NetworkDeployment | null {
|
||||||
|
const registry = readRegistry();
|
||||||
|
return registry.networks[network]?.deployments?.[contractId] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get proxy address for a contract on a network
|
||||||
|
*/
|
||||||
|
export function getProxyAddress(contractId: string, network: SupportedNetwork): string {
|
||||||
|
const deployment = getNetworkDeployment(contractId, network);
|
||||||
|
if (!deployment?.proxy) {
|
||||||
|
throw new Error(`No proxy address found for '${contractId}' on network '${network}'`);
|
||||||
|
}
|
||||||
|
return deployment.proxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current version for a contract on a network
|
||||||
|
*/
|
||||||
|
export function getCurrentVersion(contractId: string, network: SupportedNetwork): string {
|
||||||
|
const deployment = getNetworkDeployment(contractId, network);
|
||||||
|
return deployment?.currentVersion || "0.0.0";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get governance config for a network
|
||||||
|
*/
|
||||||
|
export function getGovernanceConfig(network: SupportedNetwork): GovernanceConfig {
|
||||||
|
const networkConfig = getNetworkConfig(network);
|
||||||
|
return networkConfig.governance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get version info from registry
|
||||||
|
*/
|
||||||
|
export function getVersionInfo(contractId: string, version: string): VersionInfo | null {
|
||||||
|
const registry = readRegistry();
|
||||||
|
return registry.versions[contractId]?.[version] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get latest version info for a contract
|
||||||
|
*/
|
||||||
|
export function getLatestVersionInfo(contractId: string): { version: string; info: VersionInfo } | null {
|
||||||
|
const registry = readRegistry();
|
||||||
|
const versions = registry.versions[contractId];
|
||||||
|
if (!versions) return null;
|
||||||
|
|
||||||
|
const versionNumbers = Object.keys(versions).sort((a, b) => compareVersions(b, a));
|
||||||
|
if (versionNumbers.length === 0) return null;
|
||||||
|
|
||||||
|
return { version: versionNumbers[0], info: versions[versionNumbers[0]] };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add new version to registry
|
||||||
|
*/
|
||||||
|
export function addVersion(
|
||||||
|
contractId: string,
|
||||||
|
network: SupportedNetwork,
|
||||||
|
version: string,
|
||||||
|
versionInfo: Omit<VersionInfo, "deployments">,
|
||||||
|
deployment: VersionDeployment,
|
||||||
|
): void {
|
||||||
|
const registry = readRegistry();
|
||||||
|
|
||||||
|
// Initialize versions object if needed
|
||||||
|
if (!registry.versions[contractId]) {
|
||||||
|
registry.versions[contractId] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add or update version info
|
||||||
|
if (!registry.versions[contractId][version]) {
|
||||||
|
registry.versions[contractId][version] = {
|
||||||
|
...versionInfo,
|
||||||
|
deployments: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
registry.versions[contractId][version].deployments[network] = deployment;
|
||||||
|
|
||||||
|
// Update network deployment
|
||||||
|
if (!registry.networks[network]) {
|
||||||
|
throw new Error(`Network '${network}' not configured`);
|
||||||
|
}
|
||||||
|
if (!registry.networks[network].deployments[contractId]) {
|
||||||
|
registry.networks[network].deployments[contractId] = {
|
||||||
|
proxy: "",
|
||||||
|
currentVersion: "",
|
||||||
|
currentImpl: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
registry.networks[network].deployments[contractId].currentVersion = version;
|
||||||
|
registry.networks[network].deployments[contractId].currentImpl = deployment.impl;
|
||||||
|
|
||||||
|
writeRegistry(registry);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update gitCommit for a specific version deployment
|
||||||
|
*/
|
||||||
|
export function updateVersionGitCommit(
|
||||||
|
contractId: string,
|
||||||
|
network: SupportedNetwork,
|
||||||
|
version: string,
|
||||||
|
gitCommit: string,
|
||||||
|
): void {
|
||||||
|
const registry = readRegistry();
|
||||||
|
|
||||||
|
if (!registry.versions[contractId]?.[version]?.deployments?.[network]) {
|
||||||
|
throw new Error(`Deployment not found: ${contractId} v${version} on ${network}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
registry.versions[contractId][version].deployments[network].gitCommit = gitCommit;
|
||||||
|
writeRegistry(registry);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update proxy address for a contract on a network
|
||||||
|
*/
|
||||||
|
export function setProxyAddress(contractId: string, network: SupportedNetwork, proxyAddress: string): void {
|
||||||
|
const registry = readRegistry();
|
||||||
|
|
||||||
|
if (!registry.networks[network]) {
|
||||||
|
throw new Error(`Network '${network}' not configured`);
|
||||||
|
}
|
||||||
|
if (!registry.networks[network].deployments[contractId]) {
|
||||||
|
registry.networks[network].deployments[contractId] = {
|
||||||
|
proxy: "",
|
||||||
|
currentVersion: "",
|
||||||
|
currentImpl: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
registry.networks[network].deployments[contractId].proxy = proxyAddress;
|
||||||
|
|
||||||
|
writeRegistry(registry);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current git commit hash
|
||||||
|
*/
|
||||||
|
export function getGitCommit(): string {
|
||||||
|
try {
|
||||||
|
return execSync("git rev-parse HEAD").toString().trim();
|
||||||
|
} catch {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get short git commit hash
|
||||||
|
*/
|
||||||
|
export function getGitCommitShort(): string {
|
||||||
|
try {
|
||||||
|
return execSync("git rev-parse --short HEAD").toString().trim();
|
||||||
|
} catch {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current git branch
|
||||||
|
*/
|
||||||
|
export function getGitBranch(): string {
|
||||||
|
try {
|
||||||
|
return execSync("git rev-parse --abbrev-ref HEAD").toString().trim();
|
||||||
|
} catch {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if there are uncommitted changes
|
||||||
|
*/
|
||||||
|
export function hasUncommittedChanges(): boolean {
|
||||||
|
try {
|
||||||
|
const status = execSync("git status --porcelain").toString().trim();
|
||||||
|
return status.length > 0;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a git tag
|
||||||
|
*/
|
||||||
|
export function createGitTag(tag: string, message: string): void {
|
||||||
|
execSync(`git tag -a ${tag} -m "${message}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse semantic version
|
||||||
|
*/
|
||||||
|
export function parseVersion(version: string): { major: number; minor: number; patch: number } {
|
||||||
|
const match = version.match(/^(\d+)\.(\d+)\.(\d+)$/);
|
||||||
|
if (!match) {
|
||||||
|
throw new Error(`Invalid version format: ${version}`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
major: parseInt(match[1]),
|
||||||
|
minor: parseInt(match[2]),
|
||||||
|
patch: parseInt(match[3]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare versions (returns 1 if a > b, -1 if a < b, 0 if equal)
|
||||||
|
*/
|
||||||
|
export function compareVersions(a: string, b: string): number {
|
||||||
|
const va = parseVersion(a);
|
||||||
|
const vb = parseVersion(b);
|
||||||
|
|
||||||
|
if (va.major !== vb.major) return va.major > vb.major ? 1 : -1;
|
||||||
|
if (va.minor !== vb.minor) return va.minor > vb.minor ? 1 : -1;
|
||||||
|
if (va.patch !== vb.patch) return va.patch > vb.patch ? 1 : -1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increment version
|
||||||
|
*/
|
||||||
|
export function incrementVersion(version: string, type: "major" | "minor" | "patch"): string {
|
||||||
|
const v = parseVersion(version);
|
||||||
|
switch (type) {
|
||||||
|
case "major":
|
||||||
|
return `${v.major + 1}.0.0`;
|
||||||
|
case "minor":
|
||||||
|
return `${v.major}.${v.minor + 1}.0`;
|
||||||
|
case "patch":
|
||||||
|
return `${v.major}.${v.minor}.${v.patch + 1}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Suggest next version based on current version
|
||||||
|
*/
|
||||||
|
export function suggestNextVersion(currentVersion: string): {
|
||||||
|
patch: string;
|
||||||
|
minor: string;
|
||||||
|
major: string;
|
||||||
|
} {
|
||||||
|
const v = parseVersion(currentVersion);
|
||||||
|
return {
|
||||||
|
patch: `${v.major}.${v.minor}.${v.patch + 1}`,
|
||||||
|
minor: `${v.major}.${v.minor + 1}.0`,
|
||||||
|
major: `${v.major + 1}.0.0`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that new version is a valid increment of current version
|
||||||
|
*/
|
||||||
|
export function validateVersionIncrement(
|
||||||
|
currentVersion: string,
|
||||||
|
newVersion: string,
|
||||||
|
): {
|
||||||
|
valid: boolean;
|
||||||
|
type: "patch" | "minor" | "major" | null;
|
||||||
|
error?: string;
|
||||||
|
} {
|
||||||
|
try {
|
||||||
|
const current = parseVersion(currentVersion);
|
||||||
|
const next = parseVersion(newVersion);
|
||||||
|
|
||||||
|
// Check if it's a valid increment
|
||||||
|
if (next.major === current.major + 1 && next.minor === 0 && next.patch === 0) {
|
||||||
|
return { valid: true, type: "major" };
|
||||||
|
}
|
||||||
|
if (next.major === current.major && next.minor === current.minor + 1 && next.patch === 0) {
|
||||||
|
return { valid: true, type: "minor" };
|
||||||
|
}
|
||||||
|
if (next.major === current.major && next.minor === current.minor && next.patch === current.patch + 1) {
|
||||||
|
return { valid: true, type: "patch" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not a valid increment
|
||||||
|
const suggested = suggestNextVersion(currentVersion);
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
type: null,
|
||||||
|
error: `Invalid version increment. Current: ${currentVersion}, Got: ${newVersion}. Valid options: ${suggested.patch} (patch), ${suggested.minor} (minor), ${suggested.major} (major)`,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
return { valid: false, type: null, error: `Invalid version format: ${e}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read version from contract file's @custom:version
|
||||||
|
*/
|
||||||
|
export function readContractVersion(contractPath: string): string | null {
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(contractPath, "utf-8");
|
||||||
|
const match = content.match(/@custom:version\s+(\d+\.\d+\.\d+)/);
|
||||||
|
return match ? match[1] : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read reinitializer version from contract's initialize function
|
||||||
|
* Looks for patterns like: reinitializer(N) or initializer
|
||||||
|
* Returns the highest reinitializer version found, or 1 if only initializer is found
|
||||||
|
*/
|
||||||
|
export function readReinitializerVersion(contractPath: string): number | null {
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(contractPath, "utf-8");
|
||||||
|
|
||||||
|
// Find all reinitializer(N) occurrences
|
||||||
|
const reinitMatches = content.matchAll(/reinitializer\s*\(\s*(\d+)\s*\)/g);
|
||||||
|
const versions: number[] = [];
|
||||||
|
|
||||||
|
for (const match of reinitMatches) {
|
||||||
|
versions.push(parseInt(match[1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (versions.length > 0) {
|
||||||
|
// Return the highest version found
|
||||||
|
return Math.max(...versions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for basic initializer modifier (equivalent to reinitializer(1))
|
||||||
|
if (content.match(/\binitializer\b/) && !content.match(/reinitializer/)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that reinitializer version matches expected version
|
||||||
|
* Expected version = previous initializer version + 1
|
||||||
|
*/
|
||||||
|
export function validateReinitializerVersion(
|
||||||
|
contractPath: string,
|
||||||
|
expectedVersion: number,
|
||||||
|
): { valid: boolean; actual: number | null; error?: string } {
|
||||||
|
const actual = readReinitializerVersion(contractPath);
|
||||||
|
|
||||||
|
if (actual === null) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
actual: null,
|
||||||
|
error: "Could not find reinitializer/initializer modifier in contract",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actual !== expectedVersion) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
actual,
|
||||||
|
error: `Reinitializer version mismatch. Expected: reinitializer(${expectedVersion}), Found: reinitializer(${actual})`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: true, actual };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update version in contract file's @custom:version
|
||||||
|
*/
|
||||||
|
export function updateContractVersion(contractPath: string, newVersion: string): boolean {
|
||||||
|
try {
|
||||||
|
let content = fs.readFileSync(contractPath, "utf-8");
|
||||||
|
const originalContent = content;
|
||||||
|
|
||||||
|
// Update @custom:version
|
||||||
|
content = content.replace(/@custom:version\s+\d+\.\d+\.\d+/, `@custom:version ${newVersion}`);
|
||||||
|
|
||||||
|
// Also update version() function if it exists
|
||||||
|
content = content.replace(/function version\(\)[^}]+return\s+"(\d+\.\d+\.\d+)"/, (match) =>
|
||||||
|
match.replace(/"\d+\.\d+\.\d+"/, `"${newVersion}"`),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (content !== originalContent) {
|
||||||
|
fs.writeFileSync(contractPath, content);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get contract file path from contract ID
|
||||||
|
*/
|
||||||
|
export function getContractFilePath(contractId: string): string | null {
|
||||||
|
const contract = getContractDefinition(contractId);
|
||||||
|
const contractName = contract.source;
|
||||||
|
|
||||||
|
// Common paths to check
|
||||||
|
const possiblePaths = [
|
||||||
|
path.join(__dirname, `../../contracts/${contractName}.sol`),
|
||||||
|
path.join(__dirname, `../../contracts/tests/${contractName}.sol`),
|
||||||
|
path.join(__dirname, `../../contracts/registry/${contractName}.sol`),
|
||||||
|
path.join(__dirname, `../../contracts/utils/${contractName}.sol`),
|
||||||
|
path.join(__dirname, `../../contracts/sdk/${contractName}.sol`),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const p of possiblePaths) {
|
||||||
|
if (fs.existsSync(p)) {
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a git commit
|
||||||
|
*/
|
||||||
|
export function gitCommit(message: string): boolean {
|
||||||
|
try {
|
||||||
|
execSync(`git add -A && git commit -m "${message}"`, { stdio: "pipe" });
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format address for display (shortened)
|
||||||
|
*/
|
||||||
|
export function shortenAddress(address: string): string {
|
||||||
|
if (!address || address.length < 10) return address;
|
||||||
|
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Safe API URL for a network
|
||||||
|
*/
|
||||||
|
export function getSafeApiUrl(network: SupportedNetwork): string {
|
||||||
|
// Safe Transaction Service API URLs (must end with /api/)
|
||||||
|
const urls: Record<SupportedNetwork, string> = {
|
||||||
|
celo: "https://safe-transaction-celo.safe.global/api/",
|
||||||
|
"celo-sepolia": "https://safe-transaction-celo.safe.global/api/", // Celo testnet uses same as mainnet
|
||||||
|
sepolia: "https://safe-transaction-sepolia.safe.global/api/",
|
||||||
|
localhost: "", // No Safe service for localhost
|
||||||
|
};
|
||||||
|
return urls[network];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get block explorer URL for a network
|
||||||
|
*/
|
||||||
|
export function getExplorerUrl(network: SupportedNetwork): string {
|
||||||
|
const urls: Record<SupportedNetwork, string> = {
|
||||||
|
celo: "https://celoscan.io",
|
||||||
|
"celo-sepolia": "https://celo-sepolia.blockscout.com",
|
||||||
|
sepolia: "https://sepolia.etherscan.io",
|
||||||
|
localhost: "http://localhost:8545", // No explorer for localhost
|
||||||
|
};
|
||||||
|
return urls[network];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all contract IDs from registry
|
||||||
|
*/
|
||||||
|
export function getContractIds(): string[] {
|
||||||
|
const registry = readRegistry();
|
||||||
|
return Object.keys(registry.contracts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if contract is deployed on network
|
||||||
|
*/
|
||||||
|
export function isDeployedOnNetwork(contractId: string, network: SupportedNetwork): boolean {
|
||||||
|
const deployment = getNetworkDeployment(contractId, network);
|
||||||
|
if (!deployment) return false;
|
||||||
|
|
||||||
|
const contract = getContractDefinition(contractId);
|
||||||
|
if (contract.type === "uups-proxy") {
|
||||||
|
return !!deployment.proxy;
|
||||||
|
}
|
||||||
|
return !!deployment.address;
|
||||||
|
}
|
||||||
158
contracts/test/foundry/MigratePCR0Manager.t.sol
Normal file
158
contracts/test/foundry/MigratePCR0Manager.t.sol
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
pragma solidity 0.8.28;
|
||||||
|
|
||||||
|
import {Test} from "forge-std/Test.sol";
|
||||||
|
import {console2} from "forge-std/console2.sol";
|
||||||
|
import {PCR0Manager} from "../../contracts/utils/PCR0Manager.sol";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @title MigratePCR0ManagerTest
|
||||||
|
* @notice Test for deploying PCR0Manager V2 with AccessControl governance
|
||||||
|
*
|
||||||
|
* This test:
|
||||||
|
* 1. Deploys new PCR0Manager with AccessControl
|
||||||
|
* 2. Adds all 7 finalized PCR0 values
|
||||||
|
* 3. Transfers roles to multisigs
|
||||||
|
* 4. Verifies deployer has no control after transfer
|
||||||
|
*
|
||||||
|
* Run with:
|
||||||
|
* forge test --match-contract MigratePCR0ManagerTest -vvv
|
||||||
|
*/
|
||||||
|
contract MigratePCR0ManagerTest is Test {
|
||||||
|
// Test accounts
|
||||||
|
address deployer;
|
||||||
|
address securityMultisig;
|
||||||
|
address operationsMultisig;
|
||||||
|
address unauthorized;
|
||||||
|
|
||||||
|
// Contracts
|
||||||
|
PCR0Manager pcr0Manager;
|
||||||
|
|
||||||
|
// Governance roles
|
||||||
|
bytes32 public constant SECURITY_ROLE = keccak256("SECURITY_ROLE");
|
||||||
|
bytes32 public constant OPERATIONS_ROLE = keccak256("OPERATIONS_ROLE");
|
||||||
|
|
||||||
|
// Finalized PCR0 values (32-byte format)
|
||||||
|
bytes[] pcr0Values;
|
||||||
|
|
||||||
|
function setUp() public {
|
||||||
|
console2.log("================================================================================");
|
||||||
|
console2.log("PCR0Manager DEPLOYMENT TEST: AccessControl Governance");
|
||||||
|
console2.log("================================================================================");
|
||||||
|
|
||||||
|
// Set up test accounts
|
||||||
|
deployer = makeAddr("deployer");
|
||||||
|
securityMultisig = makeAddr("securityMultisig");
|
||||||
|
operationsMultisig = makeAddr("operationsMultisig");
|
||||||
|
unauthorized = makeAddr("unauthorized");
|
||||||
|
|
||||||
|
vm.deal(deployer, 100 ether);
|
||||||
|
|
||||||
|
console2.log("Deployer:", deployer);
|
||||||
|
console2.log("Critical Multisig:", securityMultisig);
|
||||||
|
console2.log("Standard Multisig:", operationsMultisig);
|
||||||
|
|
||||||
|
// Populate finalized PCR0 values (32-byte format)
|
||||||
|
pcr0Values.push(hex"eb71776987d5f057030823f591d160c9d5d5e0a96c9a2a826778be1da2b8302a");
|
||||||
|
pcr0Values.push(hex"d2221a0ee83901980c607ceff2edbedf3f6ce5f437eafa5d89be39e9e7487c04");
|
||||||
|
pcr0Values.push(hex"4458aeb87796e92700be2d9c2984e376bce42bd80a4bf679e060d3bdaa6de119");
|
||||||
|
pcr0Values.push(hex"aa3deefa408710420e8b4ffe5b95f1dafeb4f06cb16ea44ec7353944671c660a");
|
||||||
|
pcr0Values.push(hex"b31e0df12cd52b961590796511d91a26364dd963c4aa727246b40513e470c232");
|
||||||
|
pcr0Values.push(hex"26bc53c698f78016ad7c326198d25d309d1487098af3f28fc55e951f903e9596");
|
||||||
|
pcr0Values.push(hex"b62720bdb510c2830cf9d58caa23912d0b214d6c278bf22e90942a6b69d272af");
|
||||||
|
}
|
||||||
|
|
||||||
|
function testDeploymentWorkflow() public {
|
||||||
|
console2.log("\n=== Step 1: Deploy PCR0Manager ===");
|
||||||
|
|
||||||
|
vm.startPrank(deployer);
|
||||||
|
pcr0Manager = new PCR0Manager();
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
console2.log("Deployed at:", address(pcr0Manager));
|
||||||
|
assertTrue(pcr0Manager.hasRole(SECURITY_ROLE, deployer), "Deployer missing SECURITY_ROLE");
|
||||||
|
assertTrue(pcr0Manager.hasRole(OPERATIONS_ROLE, deployer), "Deployer missing OPERATIONS_ROLE");
|
||||||
|
|
||||||
|
console2.log("\n=== Step 2: Add PCR0 Values ===");
|
||||||
|
|
||||||
|
// Add PCR0 values
|
||||||
|
vm.startPrank(deployer);
|
||||||
|
for (uint256 i = 0; i < pcr0Values.length; i++) {
|
||||||
|
pcr0Manager.addPCR0(pcr0Values[i]);
|
||||||
|
}
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
console2.log("Added", pcr0Values.length, "PCR0 values");
|
||||||
|
|
||||||
|
// Verify all PCR0s are set (check with 48-byte format)
|
||||||
|
for (uint256 i = 0; i < pcr0Values.length; i++) {
|
||||||
|
bytes memory pcr0_48 = abi.encodePacked(new bytes(16), pcr0Values[i]);
|
||||||
|
assertTrue(pcr0Manager.isPCR0Set(pcr0_48), "PCR0 not set");
|
||||||
|
}
|
||||||
|
|
||||||
|
console2.log("\n=== Step 3: Test Governance ===");
|
||||||
|
|
||||||
|
// Test add/remove functionality
|
||||||
|
vm.startPrank(deployer);
|
||||||
|
bytes memory testPCR0_32 = hex"1111111111111111111111111111111111111111111111111111111111111111";
|
||||||
|
bytes memory testPCR0_48 = abi.encodePacked(new bytes(16), testPCR0_32);
|
||||||
|
pcr0Manager.addPCR0(testPCR0_32);
|
||||||
|
assertTrue(pcr0Manager.isPCR0Set(testPCR0_48), "Test PCR0 not added");
|
||||||
|
pcr0Manager.removePCR0(testPCR0_32);
|
||||||
|
assertFalse(pcr0Manager.isPCR0Set(testPCR0_48), "Test PCR0 not removed");
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
// Unauthorized user blocked
|
||||||
|
vm.startPrank(unauthorized);
|
||||||
|
vm.expectRevert();
|
||||||
|
pcr0Manager.addPCR0(testPCR0_32);
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
console2.log("Governance working correctly");
|
||||||
|
|
||||||
|
console2.log("\n=== Step 4: Transfer Roles to Multisigs ===");
|
||||||
|
|
||||||
|
vm.startPrank(deployer);
|
||||||
|
pcr0Manager.grantRole(SECURITY_ROLE, securityMultisig);
|
||||||
|
pcr0Manager.grantRole(OPERATIONS_ROLE, operationsMultisig);
|
||||||
|
pcr0Manager.renounceRole(SECURITY_ROLE, deployer);
|
||||||
|
pcr0Manager.renounceRole(OPERATIONS_ROLE, deployer);
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
console2.log("Roles transferred to multisigs");
|
||||||
|
|
||||||
|
console2.log("\n=== Step 5: Verify Final State ===");
|
||||||
|
|
||||||
|
// Deployer has no roles
|
||||||
|
assertFalse(pcr0Manager.hasRole(SECURITY_ROLE, deployer), "Deployer still has SECURITY_ROLE");
|
||||||
|
assertFalse(pcr0Manager.hasRole(OPERATIONS_ROLE, deployer), "Deployer still has OPERATIONS_ROLE");
|
||||||
|
|
||||||
|
// Multisigs have roles
|
||||||
|
assertTrue(pcr0Manager.hasRole(SECURITY_ROLE, securityMultisig), "Critical multisig missing SECURITY_ROLE");
|
||||||
|
assertTrue(
|
||||||
|
pcr0Manager.hasRole(OPERATIONS_ROLE, operationsMultisig),
|
||||||
|
"Standard multisig missing OPERATIONS_ROLE"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Multisig can manage, deployer cannot
|
||||||
|
vm.startPrank(securityMultisig);
|
||||||
|
bytes memory testPCR0_32_v2 = hex"2222222222222222222222222222222222222222222222222222222222222222";
|
||||||
|
bytes memory testPCR0_48_v2 = abi.encodePacked(new bytes(16), testPCR0_32_v2);
|
||||||
|
pcr0Manager.addPCR0(testPCR0_32_v2);
|
||||||
|
assertTrue(pcr0Manager.isPCR0Set(testPCR0_48_v2), "Multisig cannot add PCR0");
|
||||||
|
pcr0Manager.removePCR0(testPCR0_32_v2);
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
vm.startPrank(deployer);
|
||||||
|
vm.expectRevert();
|
||||||
|
pcr0Manager.addPCR0(testPCR0_32_v2);
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
console2.log("Multisigs have full control");
|
||||||
|
console2.log("Deployer has ZERO control");
|
||||||
|
|
||||||
|
console2.log("\n================================================================================");
|
||||||
|
console2.log("DEPLOYMENT TEST PASSED - Ready for production");
|
||||||
|
console2.log("================================================================================");
|
||||||
|
}
|
||||||
|
}
|
||||||
355
contracts/test/foundry/UpgradeToAccessControl.t.sol
Normal file
355
contracts/test/foundry/UpgradeToAccessControl.t.sol
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
pragma solidity 0.8.28;
|
||||||
|
|
||||||
|
import {Test} from "forge-std/Test.sol";
|
||||||
|
import {console2} from "forge-std/console2.sol";
|
||||||
|
import {Upgrades, Options} from "openzeppelin-foundry-upgrades/Upgrades.sol";
|
||||||
|
import {IdentityVerificationHubImplV2} from "../../contracts/IdentityVerificationHubImplV2.sol";
|
||||||
|
import {IdentityRegistryImplV1} from "../../contracts/registry/IdentityRegistryImplV1.sol";
|
||||||
|
import {IdentityRegistryIdCardImplV1} from "../../contracts/registry/IdentityRegistryIdCardImplV1.sol";
|
||||||
|
import {IdentityRegistryAadhaarImplV1} from "../../contracts/registry/IdentityRegistryAadhaarImplV1.sol";
|
||||||
|
import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @title UpgradeToAccessControlTest
|
||||||
|
* @notice Fork test for upgrading contracts from Ownable to AccessControl
|
||||||
|
*
|
||||||
|
* This test:
|
||||||
|
* 1. Forks Celo mainnet at current block
|
||||||
|
* 2. Captures pre-upgrade state from real deployed contracts
|
||||||
|
* 3. Executes upgrades to AccessControl governance
|
||||||
|
* 4. Verifies ALL state is preserved (no storage collisions)
|
||||||
|
* 5. Tests governance functionality
|
||||||
|
* 6. Simulates role transfer to multisigs
|
||||||
|
* 7. Verifies deployer has no control after transfer
|
||||||
|
*
|
||||||
|
* Run with:
|
||||||
|
* forge test --match-contract UpgradeToAccessControlTest --fork-url $CELO_RPC_URL -vvv
|
||||||
|
*/
|
||||||
|
contract UpgradeToAccessControlTest is Test {
|
||||||
|
// ============================================================================
|
||||||
|
// DEPLOYED CONTRACT ADDRESSES (Celo Mainnet)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
address constant HUB_PROXY = 0xe57F4773bd9c9d8b6Cd70431117d353298B9f5BF;
|
||||||
|
address constant REGISTRY_PASSPORT_PROXY = 0x37F5CB8cB1f6B00aa768D8aA99F1A9289802A968;
|
||||||
|
address constant REGISTRY_ID_CARD_PROXY = 0xeAD1E6Ec29c1f3D33a0662f253a3a94D189566E1;
|
||||||
|
address constant REGISTRY_AADHAAR_PROXY = 0xd603Fa8C8f4694E8DD1DcE1f27C0C3fc91e32Ac4;
|
||||||
|
address constant CUSTOM_VERIFIER = 0x9E66B82Da87309fAE1403078d498a069A30860c4;
|
||||||
|
address constant POSEIDON_T3 = 0xF134707a4C4a3a76b8410fC0294d620A7c341581;
|
||||||
|
|
||||||
|
// Test accounts
|
||||||
|
address deployer;
|
||||||
|
address securityMultisig;
|
||||||
|
address operationsMultisig;
|
||||||
|
|
||||||
|
// Contracts
|
||||||
|
IdentityVerificationHubImplV2 hub;
|
||||||
|
IdentityRegistryImplV1 passportRegistry;
|
||||||
|
IdentityRegistryIdCardImplV1 idCardRegistry;
|
||||||
|
IdentityRegistryAadhaarImplV1 aadhaarRegistry;
|
||||||
|
|
||||||
|
// Governance roles
|
||||||
|
bytes32 public constant SECURITY_ROLE = keccak256("SECURITY_ROLE");
|
||||||
|
bytes32 public constant OPERATIONS_ROLE = keccak256("OPERATIONS_ROLE");
|
||||||
|
|
||||||
|
// Pre-upgrade state - captures ALL publicly accessible critical state variables
|
||||||
|
struct PreUpgradeState {
|
||||||
|
// Hub state (4 variables)
|
||||||
|
address hubRegistryPassport;
|
||||||
|
address hubRegistryIdCard;
|
||||||
|
address hubRegistryAadhaar;
|
||||||
|
uint256 hubAadhaarWindow;
|
||||||
|
// Passport Registry state (6 variables)
|
||||||
|
uint256 passportIdentityRoot;
|
||||||
|
uint256 passportDscKeyRoot;
|
||||||
|
uint256 passportPassportNoOfacRoot;
|
||||||
|
uint256 passportNameDobOfacRoot;
|
||||||
|
uint256 passportNameYobOfacRoot;
|
||||||
|
uint256 passportCscaRoot;
|
||||||
|
// ID Card Registry state (5 variables)
|
||||||
|
uint256 idCardIdentityRoot;
|
||||||
|
uint256 idCardDscKeyRoot;
|
||||||
|
uint256 idCardNameDobOfacRoot;
|
||||||
|
uint256 idCardNameYobOfacRoot;
|
||||||
|
uint256 idCardCscaRoot;
|
||||||
|
// Aadhaar Registry state (3 variables)
|
||||||
|
uint256 aadhaarIdentityRoot;
|
||||||
|
uint256 aadhaarNameDobOfacRoot;
|
||||||
|
uint256 aadhaarNameYobOfacRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
PreUpgradeState preState;
|
||||||
|
|
||||||
|
function setUp() public {
|
||||||
|
console2.log("================================================================================");
|
||||||
|
console2.log("CELO MAINNET FORK TEST: Ownable -> AccessControl Upgrade");
|
||||||
|
console2.log("================================================================================");
|
||||||
|
|
||||||
|
// Initialize contract references to get current owner
|
||||||
|
hub = IdentityVerificationHubImplV2(HUB_PROXY);
|
||||||
|
passportRegistry = IdentityRegistryImplV1(REGISTRY_PASSPORT_PROXY);
|
||||||
|
idCardRegistry = IdentityRegistryIdCardImplV1(REGISTRY_ID_CARD_PROXY);
|
||||||
|
aadhaarRegistry = IdentityRegistryAadhaarImplV1(REGISTRY_AADHAAR_PROXY);
|
||||||
|
|
||||||
|
// Get the actual current owner from the deployed contracts
|
||||||
|
deployer = Ownable2StepUpgradeable(address(hub)).owner();
|
||||||
|
|
||||||
|
// Set up multisig accounts for testing role transfer
|
||||||
|
securityMultisig = makeAddr("securityMultisig");
|
||||||
|
operationsMultisig = makeAddr("operationsMultisig");
|
||||||
|
|
||||||
|
vm.deal(deployer, 100 ether);
|
||||||
|
|
||||||
|
console2.log("Current Owner (will execute upgrade):", deployer);
|
||||||
|
console2.log("Critical Multisig (will receive roles):", securityMultisig);
|
||||||
|
console2.log("Standard Multisig (will receive roles):", operationsMultisig);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testFullUpgradeWorkflow() public {
|
||||||
|
console2.log("\n=== Phase 1: Capture Pre-Upgrade State (ALL Accessible State Variables) ===");
|
||||||
|
|
||||||
|
// Hub state (4 variables)
|
||||||
|
preState.hubRegistryPassport = hub.registry(bytes32("e_passport"));
|
||||||
|
preState.hubRegistryIdCard = hub.registry(bytes32("eu_id_card"));
|
||||||
|
preState.hubRegistryAadhaar = hub.registry(bytes32("aadhaar"));
|
||||||
|
preState.hubAadhaarWindow = hub.AADHAAR_REGISTRATION_WINDOW();
|
||||||
|
|
||||||
|
// Passport Registry state (6 variables)
|
||||||
|
preState.passportIdentityRoot = passportRegistry.getIdentityCommitmentMerkleRoot();
|
||||||
|
preState.passportDscKeyRoot = passportRegistry.getDscKeyCommitmentMerkleRoot();
|
||||||
|
preState.passportPassportNoOfacRoot = passportRegistry.getPassportNoOfacRoot();
|
||||||
|
preState.passportNameDobOfacRoot = passportRegistry.getNameAndDobOfacRoot();
|
||||||
|
preState.passportNameYobOfacRoot = passportRegistry.getNameAndYobOfacRoot();
|
||||||
|
preState.passportCscaRoot = passportRegistry.getCscaRoot();
|
||||||
|
|
||||||
|
// ID Card Registry state (5 variables)
|
||||||
|
preState.idCardIdentityRoot = idCardRegistry.getIdentityCommitmentMerkleRoot();
|
||||||
|
preState.idCardDscKeyRoot = idCardRegistry.getDscKeyCommitmentMerkleRoot();
|
||||||
|
preState.idCardNameDobOfacRoot = idCardRegistry.getNameAndDobOfacRoot();
|
||||||
|
preState.idCardNameYobOfacRoot = idCardRegistry.getNameAndYobOfacRoot();
|
||||||
|
preState.idCardCscaRoot = idCardRegistry.getCscaRoot();
|
||||||
|
|
||||||
|
// Aadhaar Registry state (3 variables)
|
||||||
|
preState.aadhaarIdentityRoot = aadhaarRegistry.getIdentityCommitmentMerkleRoot();
|
||||||
|
preState.aadhaarNameDobOfacRoot = aadhaarRegistry.getNameAndDobOfacRoot();
|
||||||
|
preState.aadhaarNameYobOfacRoot = aadhaarRegistry.getNameAndYobOfacRoot();
|
||||||
|
|
||||||
|
console2.log("Captured Hub state: 4 variables");
|
||||||
|
console2.log("Captured Passport Registry state: 6 variables");
|
||||||
|
console2.log("Captured ID Card Registry state: 5 variables");
|
||||||
|
console2.log("Captured Aadhaar Registry state: 3 variables");
|
||||||
|
console2.log("Total state variables captured: 18");
|
||||||
|
|
||||||
|
console2.log("\n=== Phase 2: Execute Upgrades ===");
|
||||||
|
vm.startPrank(deployer);
|
||||||
|
|
||||||
|
// Upgrade Hub
|
||||||
|
console2.log("Upgrading Hub...");
|
||||||
|
Options memory hubOpts;
|
||||||
|
// Skip ALL OpenZeppelin checks because:
|
||||||
|
// 1. We're changing base contracts (Ownable->AccessControl) which confuses the validator
|
||||||
|
// 2. ERC-7201 namespaced storage prevents any collision
|
||||||
|
// 3. We COMPREHENSIVELY verify safety in this test:
|
||||||
|
// - Phase 3: State preservation (no data loss)
|
||||||
|
// - Phase 3.5: Library linkage (same addresses)
|
||||||
|
// - Phase 4-6: Governance functionality (roles work correctly)
|
||||||
|
hubOpts.unsafeSkipAllChecks = true;
|
||||||
|
Upgrades.upgradeProxy(
|
||||||
|
HUB_PROXY,
|
||||||
|
"IdentityVerificationHubImplV2.sol",
|
||||||
|
abi.encodeCall(IdentityVerificationHubImplV2.initializeGovernance, ()),
|
||||||
|
hubOpts
|
||||||
|
);
|
||||||
|
|
||||||
|
// Upgrade Passport Registry
|
||||||
|
console2.log("Upgrading Passport Registry...");
|
||||||
|
Options memory passportOpts;
|
||||||
|
passportOpts.unsafeSkipAllChecks = true; // Safe: verified in test phases 3-6
|
||||||
|
Upgrades.upgradeProxy(
|
||||||
|
REGISTRY_PASSPORT_PROXY,
|
||||||
|
"IdentityRegistryImplV1.sol:IdentityRegistryImplV1",
|
||||||
|
abi.encodeCall(IdentityRegistryImplV1.initializeGovernance, ()),
|
||||||
|
passportOpts
|
||||||
|
);
|
||||||
|
|
||||||
|
// Upgrade ID Card Registry
|
||||||
|
console2.log("Upgrading ID Card Registry...");
|
||||||
|
Options memory idCardOpts;
|
||||||
|
idCardOpts.unsafeSkipAllChecks = true; // Safe: verified in test phases 3-6
|
||||||
|
Upgrades.upgradeProxy(
|
||||||
|
REGISTRY_ID_CARD_PROXY,
|
||||||
|
"IdentityRegistryIdCardImplV1.sol:IdentityRegistryIdCardImplV1",
|
||||||
|
abi.encodeCall(IdentityRegistryIdCardImplV1.initializeGovernance, ()),
|
||||||
|
idCardOpts
|
||||||
|
);
|
||||||
|
|
||||||
|
// Upgrade Aadhaar Registry
|
||||||
|
console2.log("Upgrading Aadhaar Registry...");
|
||||||
|
Options memory aadhaarOpts;
|
||||||
|
aadhaarOpts.unsafeSkipAllChecks = true; // Safe: verified in test phases 3-6
|
||||||
|
Upgrades.upgradeProxy(
|
||||||
|
REGISTRY_AADHAAR_PROXY,
|
||||||
|
"IdentityRegistryAadhaarImplV1.sol:IdentityRegistryAadhaarImplV1",
|
||||||
|
abi.encodeCall(IdentityRegistryAadhaarImplV1.initializeGovernance, ()),
|
||||||
|
aadhaarOpts
|
||||||
|
);
|
||||||
|
|
||||||
|
vm.stopPrank();
|
||||||
|
console2.log("All upgrades completed");
|
||||||
|
|
||||||
|
console2.log("\n=== Phase 3: Verify State Preservation (ALL 18 State Variables) ===");
|
||||||
|
|
||||||
|
// Hub state verification (4 variables)
|
||||||
|
assertEq(hub.registry(bytes32("e_passport")), preState.hubRegistryPassport, "Hub passport registry changed");
|
||||||
|
assertEq(hub.registry(bytes32("eu_id_card")), preState.hubRegistryIdCard, "Hub ID card registry changed");
|
||||||
|
assertEq(hub.registry(bytes32("aadhaar")), preState.hubRegistryAadhaar, "Hub aadhaar registry changed");
|
||||||
|
assertEq(hub.AADHAAR_REGISTRATION_WINDOW(), preState.hubAadhaarWindow, "Hub aadhaar window changed");
|
||||||
|
console2.log("Hub state: 4/4 variables preserved");
|
||||||
|
|
||||||
|
// Passport Registry state verification (6 variables)
|
||||||
|
assertEq(
|
||||||
|
passportRegistry.getIdentityCommitmentMerkleRoot(),
|
||||||
|
preState.passportIdentityRoot,
|
||||||
|
"Passport identity root changed"
|
||||||
|
);
|
||||||
|
assertEq(
|
||||||
|
passportRegistry.getDscKeyCommitmentMerkleRoot(),
|
||||||
|
preState.passportDscKeyRoot,
|
||||||
|
"Passport DSC key root changed"
|
||||||
|
);
|
||||||
|
assertEq(
|
||||||
|
passportRegistry.getPassportNoOfacRoot(),
|
||||||
|
preState.passportPassportNoOfacRoot,
|
||||||
|
"Passport passport# OFAC root changed"
|
||||||
|
);
|
||||||
|
assertEq(
|
||||||
|
passportRegistry.getNameAndDobOfacRoot(),
|
||||||
|
preState.passportNameDobOfacRoot,
|
||||||
|
"Passport name+DOB OFAC root changed"
|
||||||
|
);
|
||||||
|
assertEq(
|
||||||
|
passportRegistry.getNameAndYobOfacRoot(),
|
||||||
|
preState.passportNameYobOfacRoot,
|
||||||
|
"Passport name+YOB OFAC root changed"
|
||||||
|
);
|
||||||
|
assertEq(passportRegistry.getCscaRoot(), preState.passportCscaRoot, "Passport CSCA root changed");
|
||||||
|
console2.log("Passport Registry: 6/6 variables preserved");
|
||||||
|
|
||||||
|
// ID Card Registry state verification (5 variables)
|
||||||
|
assertEq(
|
||||||
|
idCardRegistry.getIdentityCommitmentMerkleRoot(),
|
||||||
|
preState.idCardIdentityRoot,
|
||||||
|
"ID Card identity root changed"
|
||||||
|
);
|
||||||
|
assertEq(
|
||||||
|
idCardRegistry.getDscKeyCommitmentMerkleRoot(),
|
||||||
|
preState.idCardDscKeyRoot,
|
||||||
|
"ID Card DSC key root changed"
|
||||||
|
);
|
||||||
|
assertEq(
|
||||||
|
idCardRegistry.getNameAndDobOfacRoot(),
|
||||||
|
preState.idCardNameDobOfacRoot,
|
||||||
|
"ID Card name+DOB OFAC root changed"
|
||||||
|
);
|
||||||
|
assertEq(
|
||||||
|
idCardRegistry.getNameAndYobOfacRoot(),
|
||||||
|
preState.idCardNameYobOfacRoot,
|
||||||
|
"ID Card name+YOB OFAC root changed"
|
||||||
|
);
|
||||||
|
assertEq(idCardRegistry.getCscaRoot(), preState.idCardCscaRoot, "ID Card CSCA root changed");
|
||||||
|
console2.log("ID Card Registry: 5/5 variables preserved");
|
||||||
|
|
||||||
|
// Aadhaar Registry state verification (3 variables)
|
||||||
|
assertEq(
|
||||||
|
aadhaarRegistry.getIdentityCommitmentMerkleRoot(),
|
||||||
|
preState.aadhaarIdentityRoot,
|
||||||
|
"Aadhaar identity root changed"
|
||||||
|
);
|
||||||
|
assertEq(
|
||||||
|
aadhaarRegistry.getNameAndDobOfacRoot(),
|
||||||
|
preState.aadhaarNameDobOfacRoot,
|
||||||
|
"Aadhaar name+DOB OFAC root changed"
|
||||||
|
);
|
||||||
|
assertEq(
|
||||||
|
aadhaarRegistry.getNameAndYobOfacRoot(),
|
||||||
|
preState.aadhaarNameYobOfacRoot,
|
||||||
|
"Aadhaar name+YOB OFAC root changed"
|
||||||
|
);
|
||||||
|
console2.log("Aadhaar Registry: 3/3 variables preserved");
|
||||||
|
|
||||||
|
console2.log("TOTAL: 18/18 state variables VERIFIED - NO storage collisions!");
|
||||||
|
|
||||||
|
console2.log("\n=== Phase 4: Verify Governance Roles ===");
|
||||||
|
|
||||||
|
// Deployer should have both roles initially
|
||||||
|
assertTrue(hub.hasRole(SECURITY_ROLE, deployer), "Deployer missing SECURITY_ROLE on Hub");
|
||||||
|
assertTrue(hub.hasRole(OPERATIONS_ROLE, deployer), "Deployer missing OPERATIONS_ROLE on Hub");
|
||||||
|
assertTrue(passportRegistry.hasRole(SECURITY_ROLE, deployer), "Deployer missing SECURITY_ROLE on Passport");
|
||||||
|
assertTrue(passportRegistry.hasRole(OPERATIONS_ROLE, deployer), "Deployer missing OPERATIONS_ROLE on Passport");
|
||||||
|
assertTrue(idCardRegistry.hasRole(SECURITY_ROLE, deployer), "Deployer missing SECURITY_ROLE on ID Card");
|
||||||
|
assertTrue(idCardRegistry.hasRole(OPERATIONS_ROLE, deployer), "Deployer missing OPERATIONS_ROLE on ID Card");
|
||||||
|
|
||||||
|
console2.log("Deployer has all required roles");
|
||||||
|
|
||||||
|
console2.log("\n=== Phase 5: Transfer Roles to Multisigs ===");
|
||||||
|
|
||||||
|
vm.startPrank(deployer);
|
||||||
|
|
||||||
|
// Grant roles to multisigs
|
||||||
|
hub.grantRole(SECURITY_ROLE, securityMultisig);
|
||||||
|
hub.grantRole(OPERATIONS_ROLE, operationsMultisig);
|
||||||
|
passportRegistry.grantRole(SECURITY_ROLE, securityMultisig);
|
||||||
|
passportRegistry.grantRole(OPERATIONS_ROLE, operationsMultisig);
|
||||||
|
idCardRegistry.grantRole(SECURITY_ROLE, securityMultisig);
|
||||||
|
idCardRegistry.grantRole(OPERATIONS_ROLE, operationsMultisig);
|
||||||
|
|
||||||
|
// Deployer renounces roles
|
||||||
|
hub.renounceRole(SECURITY_ROLE, deployer);
|
||||||
|
hub.renounceRole(OPERATIONS_ROLE, deployer);
|
||||||
|
passportRegistry.renounceRole(SECURITY_ROLE, deployer);
|
||||||
|
passportRegistry.renounceRole(OPERATIONS_ROLE, deployer);
|
||||||
|
idCardRegistry.renounceRole(SECURITY_ROLE, deployer);
|
||||||
|
idCardRegistry.renounceRole(OPERATIONS_ROLE, deployer);
|
||||||
|
|
||||||
|
vm.stopPrank();
|
||||||
|
|
||||||
|
console2.log("Roles transferred to multisigs");
|
||||||
|
|
||||||
|
console2.log("\n=== Phase 6: Verify Final State ===");
|
||||||
|
|
||||||
|
// Deployer should have NO roles
|
||||||
|
assertFalse(hub.hasRole(SECURITY_ROLE, deployer), "Deployer still has SECURITY_ROLE on Hub");
|
||||||
|
assertFalse(hub.hasRole(OPERATIONS_ROLE, deployer), "Deployer still has OPERATIONS_ROLE on Hub");
|
||||||
|
|
||||||
|
// Multisigs should have roles
|
||||||
|
assertTrue(hub.hasRole(SECURITY_ROLE, securityMultisig), "Critical multisig missing SECURITY_ROLE on Hub");
|
||||||
|
assertTrue(
|
||||||
|
hub.hasRole(OPERATIONS_ROLE, operationsMultisig),
|
||||||
|
"Standard multisig missing OPERATIONS_ROLE on Hub"
|
||||||
|
);
|
||||||
|
assertTrue(
|
||||||
|
passportRegistry.hasRole(SECURITY_ROLE, securityMultisig),
|
||||||
|
"Critical multisig missing SECURITY_ROLE on Passport"
|
||||||
|
);
|
||||||
|
assertTrue(
|
||||||
|
passportRegistry.hasRole(OPERATIONS_ROLE, operationsMultisig),
|
||||||
|
"Standard multisig missing OPERATIONS_ROLE on Passport"
|
||||||
|
);
|
||||||
|
assertTrue(
|
||||||
|
idCardRegistry.hasRole(SECURITY_ROLE, securityMultisig),
|
||||||
|
"Critical multisig missing SECURITY_ROLE on ID Card"
|
||||||
|
);
|
||||||
|
assertTrue(
|
||||||
|
idCardRegistry.hasRole(OPERATIONS_ROLE, operationsMultisig),
|
||||||
|
"Standard multisig missing OPERATIONS_ROLE on ID Card"
|
||||||
|
);
|
||||||
|
|
||||||
|
console2.log("Multisigs have full control");
|
||||||
|
console2.log("Deployer has ZERO control");
|
||||||
|
|
||||||
|
console2.log("\n================================================================================");
|
||||||
|
console2.log("UPGRADE TEST PASSED - Safe to execute on mainnet");
|
||||||
|
console2.log("================================================================================");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,27 +2,55 @@ import { expect } from "chai";
|
|||||||
import { BigNumberish, TransactionReceipt } from "ethers";
|
import { BigNumberish, TransactionReceipt } from "ethers";
|
||||||
import { ethers } from "hardhat";
|
import { ethers } from "hardhat";
|
||||||
import { poseidon2 } from "poseidon-lite";
|
import { poseidon2 } from "poseidon-lite";
|
||||||
|
import { createHash } from "crypto";
|
||||||
import { CIRCUIT_CONSTANTS, DscVerifierId, RegisterVerifierId } from "@selfxyz/common/constants/constants";
|
import { CIRCUIT_CONSTANTS, DscVerifierId, RegisterVerifierId } from "@selfxyz/common/constants/constants";
|
||||||
import { formatCountriesList, reverseBytes } from "@selfxyz/common/utils/circuits/formatInputs";
|
import { formatCountriesList, reverseBytes } from "@selfxyz/common/utils/circuits/formatInputs";
|
||||||
import { castFromScope } from "@selfxyz/common/utils/circuits/uuid";
|
import { castFromScope } from "@selfxyz/common/utils/circuits/uuid";
|
||||||
import { ATTESTATION_ID } from "../utils/constants";
|
import { ATTESTATION_ID } from "../utils/constants";
|
||||||
import { deploySystemFixtures } from "../utils/deployment";
|
import { deploySystemFixturesV2 } from "../utils/deploymentV2";
|
||||||
import BalanceTree from "../utils/example/balance-tree";
|
import BalanceTree from "../utils/example/balance-tree";
|
||||||
import { Formatter } from "../utils/formatter";
|
import { Formatter } from "../utils/formatter";
|
||||||
import { generateDscProof, generateRegisterProof, generateVcAndDiscloseProof } from "../utils/generateProof";
|
import { generateDscProof, generateRegisterProof, generateVcAndDiscloseProof } from "../utils/generateProof";
|
||||||
import { LeanIMT } from "@openpassport/zk-kit-lean-imt";
|
|
||||||
import serialized_dsc_tree from "../../../common/pubkeys/serialized_dsc_tree.json";
|
import serialized_dsc_tree from "../../../common/pubkeys/serialized_dsc_tree.json";
|
||||||
import { DeployedActors, VcAndDiscloseHubProof } from "../utils/types";
|
import { DeployedActorsV2 } from "../utils/types";
|
||||||
import { generateRandomFieldElement, splitHexFromBack } from "../utils/utils";
|
import { generateRandomFieldElement, splitHexFromBack } from "../utils/utils";
|
||||||
|
|
||||||
|
// Helper function to calculate user identifier hash
|
||||||
|
function calculateUserIdentifierHash(userContextData: string): string {
|
||||||
|
const sha256Hash = createHash("sha256")
|
||||||
|
.update(Buffer.from(userContextData.slice(2), "hex"))
|
||||||
|
.digest();
|
||||||
|
const ripemdHash = createHash("ripemd160").update(sha256Hash).digest();
|
||||||
|
return "0x" + ripemdHash.toString("hex").padStart(40, "0");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to create V2 proof data format for verifySelfProof
|
||||||
|
function createV2ProofData(proof: any, userAddress: string, userData: string = "airdrop-user-data") {
|
||||||
|
const destChainId = ethers.zeroPadValue(ethers.toBeHex(31337), 32);
|
||||||
|
const userContextData = ethers.solidityPacked(
|
||||||
|
["bytes32", "bytes32", "bytes"],
|
||||||
|
[destChainId, ethers.zeroPadValue(userAddress, 32), ethers.toUtf8Bytes(userData)],
|
||||||
|
);
|
||||||
|
|
||||||
|
const attestationId = ethers.zeroPadValue(ethers.toBeHex(BigInt(ATTESTATION_ID.E_PASSPORT)), 32);
|
||||||
|
const encodedProof = ethers.AbiCoder.defaultAbiCoder().encode(
|
||||||
|
["tuple(uint256[2] a, uint256[2][2] b, uint256[2] c, uint256[] pubSignals)"],
|
||||||
|
[[proof.a, proof.b, proof.c, proof.pubSignals]],
|
||||||
|
);
|
||||||
|
|
||||||
|
const proofData = ethers.solidityPacked(["bytes32", "bytes"], [attestationId, encodedProof]);
|
||||||
|
|
||||||
|
return { proofData, userContextData };
|
||||||
|
}
|
||||||
|
|
||||||
describe("End to End Tests", function () {
|
describe("End to End Tests", function () {
|
||||||
this.timeout(0);
|
this.timeout(0);
|
||||||
|
|
||||||
let deployedActors: DeployedActors;
|
let deployedActors: DeployedActorsV2;
|
||||||
let snapshotId: string;
|
let snapshotId: string;
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
deployedActors = await deploySystemFixtures();
|
deployedActors = await deploySystemFixturesV2();
|
||||||
snapshotId = await ethers.provider.send("evm_snapshot", []);
|
snapshotId = await ethers.provider.send("evm_snapshot", []);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -32,7 +60,10 @@ describe("End to End Tests", function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("register dsc key commitment, register identity commitment, verify commitment and disclose attrs and claim airdrop", async () => {
|
it("register dsc key commitment, register identity commitment, verify commitment and disclose attrs and claim airdrop", async () => {
|
||||||
const { hub, registry, mockPassport, owner, user1 } = deployedActors;
|
const { hub, registry, mockPassport, owner, user1, testSelfVerificationRoot, poseidonT3 } = deployedActors;
|
||||||
|
|
||||||
|
// V2 hub requires attestationId as bytes32
|
||||||
|
const attestationIdBytes32 = ethers.zeroPadValue(ethers.toBeHex(BigInt(ATTESTATION_ID.E_PASSPORT)), 32);
|
||||||
|
|
||||||
// register dsc key
|
// register dsc key
|
||||||
// To increase test performance, we will just set one dsc key with groth16 proof
|
// To increase test performance, we will just set one dsc key with groth16 proof
|
||||||
@@ -45,7 +76,11 @@ describe("End to End Tests", function () {
|
|||||||
if (BigInt(dscKeys[0][i]) == dscProof.pubSignals[CIRCUIT_CONSTANTS.DSC_TREE_LEAF_INDEX]) {
|
if (BigInt(dscKeys[0][i]) == dscProof.pubSignals[CIRCUIT_CONSTANTS.DSC_TREE_LEAF_INDEX]) {
|
||||||
const previousRoot = await registry.getDscKeyCommitmentMerkleRoot();
|
const previousRoot = await registry.getDscKeyCommitmentMerkleRoot();
|
||||||
const previousSize = await registry.getDscKeyCommitmentTreeSize();
|
const previousSize = await registry.getDscKeyCommitmentTreeSize();
|
||||||
registerDscTx = await hub.registerDscKeyCommitment(DscVerifierId.dsc_sha256_rsa_65537_4096, dscProof);
|
registerDscTx = await hub.registerDscKeyCommitment(
|
||||||
|
attestationIdBytes32,
|
||||||
|
DscVerifierId.dsc_sha256_rsa_65537_4096,
|
||||||
|
dscProof,
|
||||||
|
);
|
||||||
const receipt = (await registerDscTx.wait()) as TransactionReceipt;
|
const receipt = (await registerDscTx.wait()) as TransactionReceipt;
|
||||||
const event = receipt?.logs.find(
|
const event = receipt?.logs.find(
|
||||||
(log) => log.topics[0] === registry.interface.getEvent("DscKeyCommitmentRegistered").topicHash,
|
(log) => log.topics[0] === registry.interface.getEvent("DscKeyCommitmentRegistered").topicHash,
|
||||||
@@ -90,7 +125,8 @@ describe("End to End Tests", function () {
|
|||||||
const imt = new LeanIMT<bigint>(hashFunction);
|
const imt = new LeanIMT<bigint>(hashFunction);
|
||||||
await imt.insert(BigInt(registerProof.pubSignals[CIRCUIT_CONSTANTS.REGISTER_COMMITMENT_INDEX]));
|
await imt.insert(BigInt(registerProof.pubSignals[CIRCUIT_CONSTANTS.REGISTER_COMMITMENT_INDEX]));
|
||||||
|
|
||||||
const tx = await hub.registerPassportCommitment(
|
const tx = await hub.registerCommitment(
|
||||||
|
attestationIdBytes32,
|
||||||
RegisterVerifierId.register_sha256_sha256_sha256_rsa_65537_4096,
|
RegisterVerifierId.register_sha256_sha256_sha256_rsa_65537_4096,
|
||||||
registerProof,
|
registerProof,
|
||||||
);
|
);
|
||||||
@@ -104,7 +140,7 @@ describe("End to End Tests", function () {
|
|||||||
registerProof.pubSignals[CIRCUIT_CONSTANTS.REGISTER_COMMITMENT_INDEX],
|
registerProof.pubSignals[CIRCUIT_CONSTANTS.REGISTER_COMMITMENT_INDEX],
|
||||||
);
|
);
|
||||||
const identityNullifier = await registry.nullifiers(
|
const identityNullifier = await registry.nullifiers(
|
||||||
ATTESTATION_ID.E_PASSPORT,
|
attestationIdBytes32,
|
||||||
registerProof.pubSignals[CIRCUIT_CONSTANTS.REGISTER_NULLIFIER_INDEX],
|
registerProof.pubSignals[CIRCUIT_CONSTANTS.REGISTER_NULLIFIER_INDEX],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -134,11 +170,25 @@ describe("End to End Tests", function () {
|
|||||||
reverseBytes(Formatter.bytesToHexString(new Uint8Array(formatCountriesList(forbiddenCountriesList)))),
|
reverseBytes(Formatter.bytesToHexString(new Uint8Array(formatCountriesList(forbiddenCountriesList)))),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Get the scope from testSelfVerificationRoot
|
||||||
|
const testRootScope = await testSelfVerificationRoot.scope();
|
||||||
|
|
||||||
|
// Calculate user identifier hash for verification
|
||||||
|
const destChainId = ethers.zeroPadValue(ethers.toBeHex(31337), 32);
|
||||||
|
const user1Address = await user1.getAddress();
|
||||||
|
const userData = ethers.toUtf8Bytes("test-user-data");
|
||||||
|
const tempUserContextData = ethers.solidityPacked(
|
||||||
|
["bytes32", "bytes32", "bytes"],
|
||||||
|
[destChainId, ethers.zeroPadValue(user1Address, 32), userData],
|
||||||
|
);
|
||||||
|
const userIdentifierHash = calculateUserIdentifierHash(tempUserContextData);
|
||||||
|
|
||||||
|
// Generate proof for V2 verification
|
||||||
const vcAndDiscloseProof = await generateVcAndDiscloseProof(
|
const vcAndDiscloseProof = await generateVcAndDiscloseProof(
|
||||||
registerSecret,
|
registerSecret,
|
||||||
BigInt(ATTESTATION_ID.E_PASSPORT).toString(),
|
BigInt(ATTESTATION_ID.E_PASSPORT).toString(),
|
||||||
mockPassport,
|
mockPassport,
|
||||||
"test-scope",
|
testRootScope.toString(),
|
||||||
new Array(88).fill("1"),
|
new Array(88).fill("1"),
|
||||||
"1",
|
"1",
|
||||||
imt,
|
imt,
|
||||||
@@ -148,59 +198,114 @@ describe("End to End Tests", function () {
|
|||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
forbiddenCountriesList,
|
forbiddenCountriesList,
|
||||||
(await user1.getAddress()).slice(2),
|
userIdentifierHash,
|
||||||
);
|
);
|
||||||
|
|
||||||
const vcAndDiscloseHubProof: VcAndDiscloseHubProof = {
|
// Set up verification config for testSelfVerificationRoot
|
||||||
|
const verificationConfigV2 = {
|
||||||
olderThanEnabled: true,
|
olderThanEnabled: true,
|
||||||
olderThan: "20",
|
olderThan: "20",
|
||||||
forbiddenCountriesEnabled: true,
|
forbiddenCountriesEnabled: true,
|
||||||
forbiddenCountriesListPacked: countriesListPacked,
|
forbiddenCountriesListPacked: countriesListPacked as [BigNumberish, BigNumberish, BigNumberish, BigNumberish],
|
||||||
ofacEnabled: [true, true, true] as [boolean, boolean, boolean],
|
ofacEnabled: [true, true, true] as [boolean, boolean, boolean],
|
||||||
vcAndDiscloseProof: vcAndDiscloseProof,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await hub.verifyVcAndDisclose(vcAndDiscloseHubProof);
|
await testSelfVerificationRoot.setVerificationConfig(verificationConfigV2);
|
||||||
|
|
||||||
expect(result.identityCommitmentRoot).to.equal(
|
// Create V2 proof format and verify via testSelfVerificationRoot
|
||||||
vcAndDiscloseProof.pubSignals[CIRCUIT_CONSTANTS.VC_AND_DISCLOSE_MERKLE_ROOT_INDEX],
|
const { proofData, userContextData: verifyUserContextData } = createV2ProofData(
|
||||||
|
vcAndDiscloseProof,
|
||||||
|
user1Address,
|
||||||
|
"test-user-data",
|
||||||
);
|
);
|
||||||
expect(result.revealedDataPacked).to.have.lengthOf(3);
|
|
||||||
expect(result.nullifier).to.equal(vcAndDiscloseProof.pubSignals[CIRCUIT_CONSTANTS.VC_AND_DISCLOSE_NULLIFIER_INDEX]);
|
// Reset test state before verification
|
||||||
expect(result.attestationId).to.equal(
|
await testSelfVerificationRoot.resetTestState();
|
||||||
vcAndDiscloseProof.pubSignals[CIRCUIT_CONSTANTS.VC_AND_DISCLOSE_ATTESTATION_ID_INDEX],
|
|
||||||
|
// Verify the proof through V2 architecture
|
||||||
|
await testSelfVerificationRoot.connect(user1).verifySelfProof(proofData, verifyUserContextData);
|
||||||
|
|
||||||
|
// Check verification was successful
|
||||||
|
expect(await testSelfVerificationRoot.verificationSuccessful()).to.equal(true);
|
||||||
|
|
||||||
|
// Get the verification output and verify it
|
||||||
|
const lastOutput = await testSelfVerificationRoot.lastOutput();
|
||||||
|
expect(lastOutput).to.not.equal("0x");
|
||||||
|
|
||||||
|
// Verify attestationId matches both the expected bytes32 and the proof pubSignals
|
||||||
|
expect(lastOutput.attestationId).to.equal(attestationIdBytes32);
|
||||||
|
expect(lastOutput.attestationId).to.equal(
|
||||||
|
ethers.zeroPadValue(
|
||||||
|
ethers.toBeHex(vcAndDiscloseProof.pubSignals[CIRCUIT_CONSTANTS.VC_AND_DISCLOSE_ATTESTATION_ID_INDEX]),
|
||||||
|
32,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
expect(result.userIdentifier).to.equal(
|
|
||||||
vcAndDiscloseProof.pubSignals[CIRCUIT_CONSTANTS.VC_AND_DISCLOSE_USER_IDENTIFIER_INDEX],
|
// Verify nullifier matches the proof pubSignals
|
||||||
|
expect(lastOutput.nullifier).to.equal(
|
||||||
|
vcAndDiscloseProof.pubSignals[CIRCUIT_CONSTANTS.VC_AND_DISCLOSE_NULLIFIER_INDEX],
|
||||||
);
|
);
|
||||||
expect(result.scope).to.equal(vcAndDiscloseProof.pubSignals[CIRCUIT_CONSTANTS.VC_AND_DISCLOSE_SCOPE_INDEX]);
|
|
||||||
for (let i = 0; i < 4; i++) {
|
// Verify userIdentifier is set
|
||||||
expect(result.forbiddenCountriesListPacked[i]).to.equal(BigInt(countriesListPacked[i]));
|
expect(lastOutput.userIdentifier).to.not.equal(0n);
|
||||||
}
|
|
||||||
|
// Verify olderThan value
|
||||||
|
expect(lastOutput.olderThan).to.equal(20n);
|
||||||
|
|
||||||
const tokenFactory = await ethers.getContractFactory("AirdropToken");
|
const tokenFactory = await ethers.getContractFactory("AirdropToken");
|
||||||
const token = await tokenFactory.connect(owner).deploy();
|
const token = await tokenFactory.connect(owner).deploy();
|
||||||
await token.waitForDeployment();
|
await token.waitForDeployment();
|
||||||
|
|
||||||
const airdropFactory = await ethers.getContractFactory("Airdrop");
|
const airdropFactory = await ethers.getContractFactory("Airdrop");
|
||||||
const airdrop = await airdropFactory.connect(owner).deploy(
|
const airdrop = await airdropFactory.connect(owner).deploy(hub.target, "test-scope", token.target);
|
||||||
hub.target,
|
|
||||||
castFromScope("test-scope"),
|
|
||||||
ATTESTATION_ID.E_PASSPORT,
|
|
||||||
token.target,
|
|
||||||
true,
|
|
||||||
20,
|
|
||||||
// @ts-expect-error
|
|
||||||
true,
|
|
||||||
countriesListPacked as [BigNumberish, BigNumberish, BigNumberish, BigNumberish],
|
|
||||||
[true, true, true],
|
|
||||||
);
|
|
||||||
await airdrop.waitForDeployment();
|
await airdrop.waitForDeployment();
|
||||||
|
|
||||||
|
// Set up verification config for the airdrop
|
||||||
|
const configTx = await hub.connect(owner).setVerificationConfigV2(verificationConfigV2);
|
||||||
|
const configReceipt = await configTx.wait();
|
||||||
|
const configId = configReceipt!.logs[0].topics[1];
|
||||||
|
|
||||||
|
// Set the config ID in the airdrop contract
|
||||||
|
await airdrop.connect(owner).setConfigId(configId);
|
||||||
|
|
||||||
await token.connect(owner).mint(airdrop.target, BigInt(1000000000000000000));
|
await token.connect(owner).mint(airdrop.target, BigInt(1000000000000000000));
|
||||||
|
|
||||||
|
// Generate proof with the airdrop's actual scope
|
||||||
|
const airdropScope = await airdrop.scope();
|
||||||
|
|
||||||
|
// Calculate the user identifier hash for the airdrop proof
|
||||||
|
const airdropUserData = ethers.toUtf8Bytes("airdrop-user-data");
|
||||||
|
const airdropTempUserContextData = ethers.solidityPacked(
|
||||||
|
["bytes32", "bytes32", "bytes"],
|
||||||
|
[destChainId, ethers.zeroPadValue(user1Address, 32), airdropUserData],
|
||||||
|
);
|
||||||
|
const airdropUserIdentifierHash = calculateUserIdentifierHash(airdropTempUserContextData);
|
||||||
|
|
||||||
|
const airdropVcAndDiscloseProof = await generateVcAndDiscloseProof(
|
||||||
|
registerSecret,
|
||||||
|
BigInt(ATTESTATION_ID.E_PASSPORT).toString(),
|
||||||
|
mockPassport,
|
||||||
|
airdropScope.toString(),
|
||||||
|
new Array(88).fill("1"),
|
||||||
|
"1",
|
||||||
|
imt,
|
||||||
|
"20",
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
forbiddenCountriesList,
|
||||||
|
airdropUserIdentifierHash,
|
||||||
|
);
|
||||||
|
|
||||||
await airdrop.connect(owner).openRegistration();
|
await airdrop.connect(owner).openRegistration();
|
||||||
await airdrop.connect(user1).verifySelfProof(vcAndDiscloseProof);
|
|
||||||
|
// Create V2 proof format for verifySelfProof
|
||||||
|
const { proofData: airdropProofData, userContextData: airdropUserContextData } = createV2ProofData(
|
||||||
|
airdropVcAndDiscloseProof,
|
||||||
|
await user1.getAddress(),
|
||||||
|
);
|
||||||
|
await airdrop.connect(user1).verifySelfProof(airdropProofData, airdropUserContextData);
|
||||||
await airdrop.connect(owner).closeRegistration();
|
await airdrop.connect(owner).closeRegistration();
|
||||||
|
|
||||||
const tree = new BalanceTree([{ account: await user1.getAddress(), amount: BigInt(1000000000000000000) }]);
|
const tree = new BalanceTree([{ account: await user1.getAddress(), amount: BigInt(1000000000000000000) }]);
|
||||||
@@ -228,19 +333,13 @@ describe("End to End Tests", function () {
|
|||||||
const isClaimed = await airdrop.claimed(await user1.getAddress());
|
const isClaimed = await airdrop.claimed(await user1.getAddress());
|
||||||
expect(isClaimed).to.be.true;
|
expect(isClaimed).to.be.true;
|
||||||
|
|
||||||
const readableData = await hub.getReadableRevealedData(
|
// Verify disclosed attributes from lastOutput
|
||||||
[result.revealedDataPacked[0], result.revealedDataPacked[1], result.revealedDataPacked[2]],
|
expect(lastOutput.issuingState).to.equal("FRA");
|
||||||
["0", "1", "2", "3", "4", "5", "6", "7", "8"],
|
expect(lastOutput.idNumber).to.equal("15AA81234");
|
||||||
);
|
expect(lastOutput.nationality).to.equal("FRA");
|
||||||
|
expect(lastOutput.dateOfBirth).to.equal("31-01-94");
|
||||||
expect(readableData[0]).to.equal("FRA");
|
expect(lastOutput.gender).to.equal("M");
|
||||||
expect(readableData[1]).to.deep.equal(["ALPHONSE HUGHUES ALBERT", "DUPONT"]);
|
expect(lastOutput.expiryDate).to.equal("31-10-40");
|
||||||
expect(readableData[2]).to.equal("15AA81234");
|
expect(lastOutput.olderThan).to.equal(20n);
|
||||||
expect(readableData[3]).to.equal("FRA");
|
|
||||||
expect(readableData[4]).to.equal("31-01-94");
|
|
||||||
expect(readableData[5]).to.equal("M");
|
|
||||||
expect(readableData[6]).to.equal("31-10-40");
|
|
||||||
expect(readableData[7]).to.equal(20n);
|
|
||||||
expect(readableData[8]).to.equal(1n);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ import { BigNumberish } from "ethers";
|
|||||||
import { generateRandomFieldElement, getStartOfDayTimestamp, splitHexFromBack } from "../utils/utils";
|
import { generateRandomFieldElement, getStartOfDayTimestamp, splitHexFromBack } from "../utils/utils";
|
||||||
import { Formatter, CircuitAttributeHandler } from "../utils/formatter";
|
import { Formatter, CircuitAttributeHandler } from "../utils/formatter";
|
||||||
import { formatCountriesList, reverseBytes, reverseCountryBytes } from "@selfxyz/common/utils/circuits/formatInputs";
|
import { formatCountriesList, reverseBytes, reverseCountryBytes } from "@selfxyz/common/utils/circuits/formatInputs";
|
||||||
import { getPackedForbiddenCountries } from "@selfxyz/common/utils/sanctions";
|
import { getPackedForbiddenCountries } from "@selfxyz/common/utils/contracts/forbiddenCountries";
|
||||||
import { countries, Country3LetterCode } from "@selfxyz/common/constants/countries";
|
import { countries, Country3LetterCode } from "@selfxyz/common/constants/countries";
|
||||||
|
import { castFromScope } from "@selfxyz/common/utils/circuits/uuid";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
describe("VC and Disclose", () => {
|
describe("VC and Disclose", () => {
|
||||||
@@ -100,7 +101,7 @@ describe("VC and Disclose", () => {
|
|||||||
registerSecret,
|
registerSecret,
|
||||||
BigInt(ATTESTATION_ID.E_PASSPORT).toString(),
|
BigInt(ATTESTATION_ID.E_PASSPORT).toString(),
|
||||||
deployedActors.mockPassport,
|
deployedActors.mockPassport,
|
||||||
"test-scope",
|
castFromScope("test-scope"),
|
||||||
new Array(88).fill("1"),
|
new Array(88).fill("1"),
|
||||||
"1",
|
"1",
|
||||||
imt,
|
imt,
|
||||||
@@ -110,7 +111,7 @@ describe("VC and Disclose", () => {
|
|||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
forbiddenCountriesList,
|
forbiddenCountriesList,
|
||||||
(await deployedActors.user1.getAddress()).slice(2),
|
await deployedActors.user1.getAddress(),
|
||||||
);
|
);
|
||||||
snapshotId = await ethers.provider.send("evm_snapshot", []);
|
snapshotId = await ethers.provider.send("evm_snapshot", []);
|
||||||
});
|
});
|
||||||
@@ -439,6 +440,7 @@ describe("VC and Disclose", () => {
|
|||||||
const { hub, registry, owner, mockPassport } = deployedActors;
|
const { hub, registry, owner, mockPassport } = deployedActors;
|
||||||
|
|
||||||
const hashFunction = (a: bigint, b: bigint) => poseidon2([a, b]);
|
const hashFunction = (a: bigint, b: bigint) => poseidon2([a, b]);
|
||||||
|
const LeanIMT = await import("@openpassport/zk-kit-lean-imt").then((mod) => mod.LeanIMT);
|
||||||
const imt = new LeanIMT<bigint>(hashFunction);
|
const imt = new LeanIMT<bigint>(hashFunction);
|
||||||
imt.insert(BigInt(commitment));
|
imt.insert(BigInt(commitment));
|
||||||
|
|
||||||
@@ -448,7 +450,7 @@ describe("VC and Disclose", () => {
|
|||||||
registerSecret,
|
registerSecret,
|
||||||
BigInt(ATTESTATION_ID.E_PASSPORT).toString(),
|
BigInt(ATTESTATION_ID.E_PASSPORT).toString(),
|
||||||
mockPassport,
|
mockPassport,
|
||||||
"test-scope",
|
castFromScope("test-scope"),
|
||||||
new Array(88).fill("1"),
|
new Array(88).fill("1"),
|
||||||
"1",
|
"1",
|
||||||
imt,
|
imt,
|
||||||
@@ -746,43 +748,60 @@ describe("VC and Disclose", () => {
|
|||||||
it("should parse forbidden countries with CircuitAttributeHandler", async () => {
|
it("should parse forbidden countries with CircuitAttributeHandler", async () => {
|
||||||
const { hub } = deployedActors;
|
const { hub } = deployedActors;
|
||||||
|
|
||||||
const forbiddenCountriesListPacked = splitHexFromBack(
|
const localForbiddenCountriesList = ["AFG", "ABC", "CBA"] as const;
|
||||||
reverseCountryBytes(Formatter.bytesToHexString(new Uint8Array(formatCountriesList(forbiddenCountriesList)))),
|
const forbiddenCountriesListPacked = getPackedForbiddenCountries([...localForbiddenCountriesList]);
|
||||||
);
|
|
||||||
const readableForbiddenCountries = await hub.getReadableForbiddenCountries(forbiddenCountriesListPacked);
|
const readableForbiddenCountries = await hub.getReadableForbiddenCountries(forbiddenCountriesListPacked);
|
||||||
|
|
||||||
expect(readableForbiddenCountries[0]).to.equal(forbiddenCountriesList[0]);
|
expect(readableForbiddenCountries[0]).to.equal(localForbiddenCountriesList[0]);
|
||||||
expect(readableForbiddenCountries[1]).to.equal(forbiddenCountriesList[1]);
|
expect(readableForbiddenCountries[1]).to.equal(localForbiddenCountriesList[1]);
|
||||||
expect(readableForbiddenCountries[2]).to.equal(forbiddenCountriesList[2]);
|
expect(readableForbiddenCountries[2]).to.equal(localForbiddenCountriesList[2]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return maximum length of forbidden countries", async () => {
|
it("should return maximum length of forbidden countries", async () => {
|
||||||
const { hub } = deployedActors;
|
const { hub } = deployedActors;
|
||||||
|
|
||||||
const forbiddenCountriesList = ["AAA", "FRA", "CBA", "CBA", "CBA", "CBA", "CBA", "CBA", "CBA", "CBA"];
|
const localForbiddenCountriesList = [
|
||||||
const forbiddenCountriesListPacked = splitHexFromBack(
|
"AAA",
|
||||||
reverseCountryBytes(Formatter.bytesToHexString(new Uint8Array(formatCountriesList(forbiddenCountriesList)))),
|
"FRA",
|
||||||
);
|
"CBA",
|
||||||
|
"CBA",
|
||||||
|
"CBA",
|
||||||
|
"CBA",
|
||||||
|
"CBA",
|
||||||
|
"CBA",
|
||||||
|
"CBA",
|
||||||
|
"CBA",
|
||||||
|
] as const;
|
||||||
|
const forbiddenCountriesListPacked = getPackedForbiddenCountries([...localForbiddenCountriesList]);
|
||||||
const readableForbiddenCountries = await hub.getReadableForbiddenCountries(forbiddenCountriesListPacked);
|
const readableForbiddenCountries = await hub.getReadableForbiddenCountries(forbiddenCountriesListPacked);
|
||||||
expect(readableForbiddenCountries.length).to.equal(40);
|
expect(readableForbiddenCountries.length).to.equal(40);
|
||||||
expect(readableForbiddenCountries[0]).to.equal(forbiddenCountriesList[0]);
|
expect(readableForbiddenCountries[0]).to.equal(localForbiddenCountriesList[0]);
|
||||||
expect(readableForbiddenCountries[1]).to.equal(forbiddenCountriesList[1]);
|
expect(readableForbiddenCountries[1]).to.equal(localForbiddenCountriesList[1]);
|
||||||
expect(readableForbiddenCountries[2]).to.equal(forbiddenCountriesList[2]);
|
expect(readableForbiddenCountries[2]).to.equal(localForbiddenCountriesList[2]);
|
||||||
expect(readableForbiddenCountries[3]).to.equal(forbiddenCountriesList[3]);
|
expect(readableForbiddenCountries[3]).to.equal(localForbiddenCountriesList[3]);
|
||||||
expect(readableForbiddenCountries[4]).to.equal(forbiddenCountriesList[4]);
|
expect(readableForbiddenCountries[4]).to.equal(localForbiddenCountriesList[4]);
|
||||||
expect(readableForbiddenCountries[5]).to.equal(forbiddenCountriesList[5]);
|
expect(readableForbiddenCountries[5]).to.equal(localForbiddenCountriesList[5]);
|
||||||
expect(readableForbiddenCountries[6]).to.equal(forbiddenCountriesList[6]);
|
expect(readableForbiddenCountries[6]).to.equal(localForbiddenCountriesList[6]);
|
||||||
expect(readableForbiddenCountries[7]).to.equal(forbiddenCountriesList[7]);
|
expect(readableForbiddenCountries[7]).to.equal(localForbiddenCountriesList[7]);
|
||||||
expect(readableForbiddenCountries[8]).to.equal(forbiddenCountriesList[8]);
|
expect(readableForbiddenCountries[8]).to.equal(localForbiddenCountriesList[8]);
|
||||||
expect(readableForbiddenCountries[9]).to.equal(forbiddenCountriesList[9]);
|
expect(readableForbiddenCountries[9]).to.equal(localForbiddenCountriesList[9]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should fail when getReadableForbiddenCountries is called by non-proxy", async () => {
|
it("should fail when getReadableForbiddenCountries is called by non-proxy", async () => {
|
||||||
const { hubImpl } = deployedActors;
|
const { hubImpl } = deployedActors;
|
||||||
const forbiddenCountriesList = ["AAA", "FRA", "CBA", "CBA", "CBA", "CBA", "CBA", "CBA", "CBA", "CBA"];
|
const localForbiddenCountriesList = [
|
||||||
const forbiddenCountriesListPacked = splitHexFromBack(
|
"AAA",
|
||||||
reverseCountryBytes(Formatter.bytesToHexString(new Uint8Array(formatCountriesList(forbiddenCountriesList)))),
|
"FRA",
|
||||||
);
|
"CBA",
|
||||||
|
"CBA",
|
||||||
|
"CBA",
|
||||||
|
"CBA",
|
||||||
|
"CBA",
|
||||||
|
"CBA",
|
||||||
|
"CBA",
|
||||||
|
"CBA",
|
||||||
|
] as const;
|
||||||
|
const forbiddenCountriesListPacked = getPackedForbiddenCountries([...localForbiddenCountriesList]);
|
||||||
await expect(hubImpl.getReadableForbiddenCountries(forbiddenCountriesListPacked)).to.be.revertedWithCustomError(
|
await expect(hubImpl.getReadableForbiddenCountries(forbiddenCountriesListPacked)).to.be.revertedWithCustomError(
|
||||||
hubImpl,
|
hubImpl,
|
||||||
"UUPSUnauthorizedCallContext",
|
"UUPSUnauthorizedCallContext",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { poseidon2 } from "poseidon-lite";
|
|||||||
import { generateVcAndDiscloseProof, parseSolidityCalldata } from "../utils/generateProof";
|
import { generateVcAndDiscloseProof, parseSolidityCalldata } from "../utils/generateProof";
|
||||||
import { Formatter } from "../utils/formatter";
|
import { Formatter } from "../utils/formatter";
|
||||||
import { formatCountriesList, reverseBytes } from "@selfxyz/common/utils/circuits/formatInputs";
|
import { formatCountriesList, reverseBytes } from "@selfxyz/common/utils/circuits/formatInputs";
|
||||||
|
import { stringToBigInt } from "@selfxyz/common/utils/scope";
|
||||||
import { VerifyAll } from "../../typechain-types";
|
import { VerifyAll } from "../../typechain-types";
|
||||||
import { getSMTs } from "../utils/generateProof";
|
import { getSMTs } from "../utils/generateProof";
|
||||||
import { Groth16Proof, PublicSignals, groth16 } from "snarkjs";
|
import { Groth16Proof, PublicSignals, groth16 } from "snarkjs";
|
||||||
@@ -102,7 +103,7 @@ describe("VerifyAll", () => {
|
|||||||
registerSecret,
|
registerSecret,
|
||||||
BigInt(ATTESTATION_ID.E_PASSPORT).toString(),
|
BigInt(ATTESTATION_ID.E_PASSPORT).toString(),
|
||||||
deployedActors.mockPassport,
|
deployedActors.mockPassport,
|
||||||
"test-scope",
|
stringToBigInt("test-scope").toString(),
|
||||||
new Array(88).fill("1"),
|
new Array(88).fill("1"),
|
||||||
"1",
|
"1",
|
||||||
imt,
|
imt,
|
||||||
@@ -112,7 +113,7 @@ describe("VerifyAll", () => {
|
|||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
forbiddenCountriesList,
|
forbiddenCountriesList,
|
||||||
(await deployedActors.user1.getAddress()).slice(2),
|
await deployedActors.user1.getAddress(),
|
||||||
);
|
);
|
||||||
snapshotId = await ethers.provider.send("evm_snapshot", []);
|
snapshotId = await ethers.provider.send("evm_snapshot", []);
|
||||||
});
|
});
|
||||||
@@ -293,7 +294,7 @@ describe("VerifyAll", () => {
|
|||||||
registerSecret,
|
registerSecret,
|
||||||
BigInt(ATTESTATION_ID.E_PASSPORT).toString(),
|
BigInt(ATTESTATION_ID.E_PASSPORT).toString(),
|
||||||
deployedActors.mockPassport,
|
deployedActors.mockPassport,
|
||||||
"test-scope",
|
stringToBigInt("test-scope").toString(),
|
||||||
new Array(88).fill("1"),
|
new Array(88).fill("1"),
|
||||||
"1",
|
"1",
|
||||||
imt,
|
imt,
|
||||||
@@ -460,7 +461,7 @@ describe("VerifyAll", () => {
|
|||||||
const newHubAddress = await deployedActors.user1.getAddress();
|
const newHubAddress = await deployedActors.user1.getAddress();
|
||||||
await expect(verifyAll.connect(deployedActors.user1).setHub(newHubAddress)).to.be.revertedWithCustomError(
|
await expect(verifyAll.connect(deployedActors.user1).setHub(newHubAddress)).to.be.revertedWithCustomError(
|
||||||
verifyAll,
|
verifyAll,
|
||||||
"OwnableUnauthorizedAccount",
|
"AccessControlUnauthorizedAccount",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -468,7 +469,7 @@ describe("VerifyAll", () => {
|
|||||||
const newRegistryAddress = await deployedActors.user1.getAddress();
|
const newRegistryAddress = await deployedActors.user1.getAddress();
|
||||||
await expect(
|
await expect(
|
||||||
verifyAll.connect(deployedActors.user1).setRegistry(newRegistryAddress),
|
verifyAll.connect(deployedActors.user1).setRegistry(newRegistryAddress),
|
||||||
).to.be.revertedWithCustomError(verifyAll, "OwnableUnauthorizedAccount");
|
).to.be.revertedWithCustomError(verifyAll, "AccessControlUnauthorizedAccount");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -357,7 +357,7 @@ describe("Unit Tests for IdentityRegistry", () => {
|
|||||||
|
|
||||||
await expect(registry.connect(user1).updateHub(newHubAddress)).to.be.revertedWithCustomError(
|
await expect(registry.connect(user1).updateHub(newHubAddress)).to.be.revertedWithCustomError(
|
||||||
registry,
|
registry,
|
||||||
"OwnableUnauthorizedAccount",
|
"AccessControlUnauthorizedAccount",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -394,7 +394,7 @@ describe("Unit Tests for IdentityRegistry", () => {
|
|||||||
expect(await registry.getNameAndYobOfacRoot()).to.equal(yobRoot);
|
expect(await registry.getNameAndYobOfacRoot()).to.equal(yobRoot);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not update OFAC root if caller is not owner", async () => {
|
it("should not update OFAC root if caller does not have OPERATIONS_ROLE", async () => {
|
||||||
const { registry, user1 } = deployedActors;
|
const { registry, user1 } = deployedActors;
|
||||||
const passportRoot = generateRandomFieldElement();
|
const passportRoot = generateRandomFieldElement();
|
||||||
const dobRoot = generateRandomFieldElement();
|
const dobRoot = generateRandomFieldElement();
|
||||||
@@ -402,15 +402,15 @@ describe("Unit Tests for IdentityRegistry", () => {
|
|||||||
|
|
||||||
await expect(registry.connect(user1).updatePassportNoOfacRoot(passportRoot)).to.be.revertedWithCustomError(
|
await expect(registry.connect(user1).updatePassportNoOfacRoot(passportRoot)).to.be.revertedWithCustomError(
|
||||||
registry,
|
registry,
|
||||||
"OwnableUnauthorizedAccount",
|
"AccessControlUnauthorizedAccount",
|
||||||
);
|
);
|
||||||
await expect(registry.connect(user1).updateNameAndDobOfacRoot(dobRoot)).to.be.revertedWithCustomError(
|
await expect(registry.connect(user1).updateNameAndDobOfacRoot(dobRoot)).to.be.revertedWithCustomError(
|
||||||
registry,
|
registry,
|
||||||
"OwnableUnauthorizedAccount",
|
"AccessControlUnauthorizedAccount",
|
||||||
);
|
);
|
||||||
await expect(registry.connect(user1).updateNameAndYobOfacRoot(yobRoot)).to.be.revertedWithCustomError(
|
await expect(registry.connect(user1).updateNameAndYobOfacRoot(yobRoot)).to.be.revertedWithCustomError(
|
||||||
registry,
|
registry,
|
||||||
"OwnableUnauthorizedAccount",
|
"AccessControlUnauthorizedAccount",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -443,13 +443,13 @@ describe("Unit Tests for IdentityRegistry", () => {
|
|||||||
expect(await registry.getCscaRoot()).to.equal(newCscaRoot);
|
expect(await registry.getCscaRoot()).to.equal(newCscaRoot);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not update CSCA root if caller is not owner", async () => {
|
it("should not update CSCA root if caller does not have OPERATIONS_ROLE", async () => {
|
||||||
const { registry, user1 } = deployedActors;
|
const { registry, user1 } = deployedActors;
|
||||||
const newCscaRoot = generateRandomFieldElement();
|
const newCscaRoot = generateRandomFieldElement();
|
||||||
|
|
||||||
await expect(registry.connect(user1).updateCscaRoot(newCscaRoot)).to.be.revertedWithCustomError(
|
await expect(registry.connect(user1).updateCscaRoot(newCscaRoot)).to.be.revertedWithCustomError(
|
||||||
registry,
|
registry,
|
||||||
"OwnableUnauthorizedAccount",
|
"AccessControlUnauthorizedAccount",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -498,7 +498,7 @@ describe("Unit Tests for IdentityRegistry", () => {
|
|||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
registry.connect(user1).devAddIdentityCommitment(attestationId, nullifier, commitment),
|
registry.connect(user1).devAddIdentityCommitment(attestationId, nullifier, commitment),
|
||||||
).to.be.revertedWithCustomError(registry, "OwnableUnauthorizedAccount");
|
).to.be.revertedWithCustomError(registry, "AccessControlUnauthorizedAccount");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not add commitment if caller is not proxy", async () => {
|
it("should not add commitment if caller is not proxy", async () => {
|
||||||
@@ -546,7 +546,7 @@ describe("Unit Tests for IdentityRegistry", () => {
|
|||||||
const newCommitment = generateRandomFieldElement();
|
const newCommitment = generateRandomFieldElement();
|
||||||
await expect(
|
await expect(
|
||||||
registry.connect(user1).devUpdateCommitment(commitment, newCommitment, []),
|
registry.connect(user1).devUpdateCommitment(commitment, newCommitment, []),
|
||||||
).to.be.revertedWithCustomError(registry, "OwnableUnauthorizedAccount");
|
).to.be.revertedWithCustomError(registry, "AccessControlUnauthorizedAccount");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not update commitment if caller is not proxy", async () => {
|
it("should not update commitment if caller is not proxy", async () => {
|
||||||
@@ -592,7 +592,7 @@ describe("Unit Tests for IdentityRegistry", () => {
|
|||||||
await registry.devAddIdentityCommitment(attestationId, nullifier, commitment);
|
await registry.devAddIdentityCommitment(attestationId, nullifier, commitment);
|
||||||
await expect(registry.connect(user1).devRemoveCommitment(commitment, [])).to.be.revertedWithCustomError(
|
await expect(registry.connect(user1).devRemoveCommitment(commitment, [])).to.be.revertedWithCustomError(
|
||||||
registry,
|
registry,
|
||||||
"OwnableUnauthorizedAccount",
|
"AccessControlUnauthorizedAccount",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -632,7 +632,7 @@ describe("Unit Tests for IdentityRegistry", () => {
|
|||||||
const dscCommitment = generateRandomFieldElement();
|
const dscCommitment = generateRandomFieldElement();
|
||||||
await expect(registry.connect(user1).devAddDscKeyCommitment(dscCommitment)).to.be.revertedWithCustomError(
|
await expect(registry.connect(user1).devAddDscKeyCommitment(dscCommitment)).to.be.revertedWithCustomError(
|
||||||
registry,
|
registry,
|
||||||
"OwnableUnauthorizedAccount",
|
"AccessControlUnauthorizedAccount",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -673,7 +673,7 @@ describe("Unit Tests for IdentityRegistry", () => {
|
|||||||
await registry.devAddDscKeyCommitment(dscCommitment);
|
await registry.devAddDscKeyCommitment(dscCommitment);
|
||||||
await expect(
|
await expect(
|
||||||
registry.connect(user1).devUpdateDscKeyCommitment(dscCommitment, newDscCommitment, []),
|
registry.connect(user1).devUpdateDscKeyCommitment(dscCommitment, newDscCommitment, []),
|
||||||
).to.be.revertedWithCustomError(registry, "OwnableUnauthorizedAccount");
|
).to.be.revertedWithCustomError(registry, "AccessControlUnauthorizedAccount");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not update dsc key commitment if caller is not proxy", async () => {
|
it("should not update dsc key commitment if caller is not proxy", async () => {
|
||||||
@@ -711,7 +711,7 @@ describe("Unit Tests for IdentityRegistry", () => {
|
|||||||
await registry.devAddDscKeyCommitment(dscCommitment);
|
await registry.devAddDscKeyCommitment(dscCommitment);
|
||||||
await expect(registry.connect(user1).devRemoveDscKeyCommitment(dscCommitment, [])).to.be.revertedWithCustomError(
|
await expect(registry.connect(user1).devRemoveDscKeyCommitment(dscCommitment, [])).to.be.revertedWithCustomError(
|
||||||
registry,
|
registry,
|
||||||
"OwnableUnauthorizedAccount",
|
"AccessControlUnauthorizedAccount",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -751,7 +751,7 @@ describe("Unit Tests for IdentityRegistry", () => {
|
|||||||
const nullifier = generateRandomFieldElement();
|
const nullifier = generateRandomFieldElement();
|
||||||
await expect(
|
await expect(
|
||||||
registry.connect(user1).devChangeNullifierState(attestationId, nullifier, false),
|
registry.connect(user1).devChangeNullifierState(attestationId, nullifier, false),
|
||||||
).to.be.revertedWithCustomError(registry, "OwnableUnauthorizedAccount");
|
).to.be.revertedWithCustomError(registry, "AccessControlUnauthorizedAccount");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not change nullifier state if caller is not proxy", async () => {
|
it("should not change nullifier state if caller is not proxy", async () => {
|
||||||
@@ -789,7 +789,7 @@ describe("Unit Tests for IdentityRegistry", () => {
|
|||||||
const state = true;
|
const state = true;
|
||||||
await expect(
|
await expect(
|
||||||
registry.connect(user1).devChangeDscKeyCommitmentState(dscCommitment, state),
|
registry.connect(user1).devChangeDscKeyCommitmentState(dscCommitment, state),
|
||||||
).to.be.revertedWithCustomError(registry, "OwnableUnauthorizedAccount");
|
).to.be.revertedWithCustomError(registry, "AccessControlUnauthorizedAccount");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not change dsc key commitment state if caller is not proxy", async () => {
|
it("should not change dsc key commitment state if caller is not proxy", async () => {
|
||||||
@@ -915,7 +915,7 @@ describe("Unit Tests for IdentityRegistry", () => {
|
|||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
registry.connect(user1).upgradeToAndCall(registryV2Implementation.target, "0x"),
|
registry.connect(user1).upgradeToAndCall(registryV2Implementation.target, "0x"),
|
||||||
).to.be.revertedWithCustomError(registry, "OwnableUnauthorizedAccount");
|
).to.be.revertedWithCustomError(registry, "AccessControlUnauthorizedAccount");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not allow implementation contract to be initialized directly", async () => {
|
it("should not allow implementation contract to be initialized directly", async () => {
|
||||||
|
|||||||
@@ -1,95 +1,272 @@
|
|||||||
import { expect } from "chai";
|
import { expect } from "chai";
|
||||||
import { ethers } from "hardhat";
|
import { ethers } from "hardhat";
|
||||||
import { ZeroAddress } from "ethers";
|
|
||||||
import { MockImplRoot } from "../../typechain-types";
|
import { MockImplRoot } from "../../typechain-types";
|
||||||
|
import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers";
|
||||||
|
|
||||||
describe("ImplRoot", () => {
|
describe("ImplRoot", () => {
|
||||||
let mockImplRoot: MockImplRoot;
|
let mockImplRoot: MockImplRoot;
|
||||||
let owner: any;
|
let deployer: SignerWithAddress;
|
||||||
let user1: any;
|
let securityMultisig: SignerWithAddress;
|
||||||
|
let operationsMultisig: SignerWithAddress;
|
||||||
|
let user1: SignerWithAddress;
|
||||||
|
|
||||||
|
const SECURITY_ROLE = ethers.keccak256(ethers.toUtf8Bytes("SECURITY_ROLE"));
|
||||||
|
const OPERATIONS_ROLE = ethers.keccak256(ethers.toUtf8Bytes("OPERATIONS_ROLE"));
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
[owner, user1] = await ethers.getSigners();
|
[deployer, securityMultisig, operationsMultisig, user1] = await ethers.getSigners();
|
||||||
|
|
||||||
const MockImplRootFactory = await ethers.getContractFactory("MockImplRoot", owner);
|
const MockImplRootFactory = await ethers.getContractFactory("MockImplRoot", deployer);
|
||||||
mockImplRoot = await MockImplRootFactory.deploy();
|
mockImplRoot = await MockImplRootFactory.deploy();
|
||||||
await mockImplRoot.waitForDeployment();
|
await mockImplRoot.waitForDeployment();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Role Constants", () => {
|
||||||
|
it("should have correct role constants", async () => {
|
||||||
|
expect(await mockImplRoot.SECURITY_ROLE()).to.equal(SECURITY_ROLE);
|
||||||
|
expect(await mockImplRoot.OPERATIONS_ROLE()).to.equal(OPERATIONS_ROLE);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("Initialization", () => {
|
describe("Initialization", () => {
|
||||||
it("should revert when calling __ImplRoot_init outside initialization phase", async () => {
|
it("should revert when calling __ImplRoot_init outside initialization phase", async () => {
|
||||||
|
// First initialize the contract properly
|
||||||
|
await mockImplRoot.exposed__ImplRoot_init();
|
||||||
|
|
||||||
|
// Then try to initialize again - this should fail
|
||||||
await expect(mockImplRoot.exposed__ImplRoot_init()).to.be.revertedWithCustomError(
|
await expect(mockImplRoot.exposed__ImplRoot_init()).to.be.revertedWithCustomError(
|
||||||
mockImplRoot,
|
|
||||||
"NotInitializing",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should revert when initializing with zero address owner", async () => {
|
|
||||||
await expect(mockImplRoot.exposed__Ownable_init(ZeroAddress))
|
|
||||||
.to.be.revertedWithCustomError(mockImplRoot, "OwnableInvalidOwner")
|
|
||||||
.withArgs(ZeroAddress);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should set correct owner when initializing with valid address", async () => {
|
|
||||||
await mockImplRoot.exposed__Ownable_init(owner.address);
|
|
||||||
expect(await mockImplRoot.owner()).to.equal(owner.address);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should revert when initializing twice", async () => {
|
|
||||||
await mockImplRoot.exposed__Ownable_init(owner.address);
|
|
||||||
|
|
||||||
await expect(mockImplRoot.exposed__Ownable_init(owner.address)).to.be.revertedWithCustomError(
|
|
||||||
mockImplRoot,
|
mockImplRoot,
|
||||||
"InvalidInitialization",
|
"InvalidInitialization",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should initialize with deployer having both roles", async () => {
|
||||||
|
// Deploy a fresh contract for initialization testing
|
||||||
|
const MockImplRootFactory = await ethers.getContractFactory("MockImplRoot");
|
||||||
|
const freshContract = await MockImplRootFactory.deploy();
|
||||||
|
await freshContract.waitForDeployment();
|
||||||
|
|
||||||
|
await freshContract.exposed__ImplRoot_init();
|
||||||
|
|
||||||
|
// Check role assignments - deployer should have both roles
|
||||||
|
expect(await freshContract.hasRole(SECURITY_ROLE, deployer.address)).to.be.true;
|
||||||
|
expect(await freshContract.hasRole(OPERATIONS_ROLE, deployer.address)).to.be.true;
|
||||||
|
|
||||||
|
// Check role admins - SECURITY_ROLE manages all roles
|
||||||
|
expect(await freshContract.getRoleAdmin(SECURITY_ROLE)).to.equal(SECURITY_ROLE);
|
||||||
|
expect(await freshContract.getRoleAdmin(OPERATIONS_ROLE)).to.equal(SECURITY_ROLE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should allow role transfer after initialization", async () => {
|
||||||
|
// Deploy a fresh contract for initialization testing
|
||||||
|
const MockImplRootFactory = await ethers.getContractFactory("MockImplRoot");
|
||||||
|
const freshContract = await MockImplRootFactory.deploy();
|
||||||
|
await freshContract.waitForDeployment();
|
||||||
|
|
||||||
|
await freshContract.exposed__ImplRoot_init();
|
||||||
|
|
||||||
|
// Transfer roles to multisigs
|
||||||
|
await freshContract.connect(deployer).grantRole(SECURITY_ROLE, securityMultisig.address);
|
||||||
|
await freshContract.connect(deployer).grantRole(OPERATIONS_ROLE, operationsMultisig.address);
|
||||||
|
|
||||||
|
// Verify multisigs have roles
|
||||||
|
expect(await freshContract.hasRole(SECURITY_ROLE, securityMultisig.address)).to.be.true;
|
||||||
|
expect(await freshContract.hasRole(OPERATIONS_ROLE, operationsMultisig.address)).to.be.true;
|
||||||
|
|
||||||
|
// Deployer can renounce roles
|
||||||
|
await freshContract.connect(deployer).renounceRole(SECURITY_ROLE, deployer.address);
|
||||||
|
await freshContract.connect(deployer).renounceRole(OPERATIONS_ROLE, deployer.address);
|
||||||
|
|
||||||
|
// Verify deployer no longer has roles
|
||||||
|
expect(await freshContract.hasRole(SECURITY_ROLE, deployer.address)).to.be.false;
|
||||||
|
expect(await freshContract.hasRole(OPERATIONS_ROLE, deployer.address)).to.be.false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Role Management", () => {
|
||||||
|
let initializedContract: MockImplRoot;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const MockImplRootFactory = await ethers.getContractFactory("MockImplRoot");
|
||||||
|
initializedContract = await MockImplRootFactory.deploy();
|
||||||
|
await initializedContract.waitForDeployment();
|
||||||
|
|
||||||
|
// Initialize with deployer having roles, then transfer to multisigs
|
||||||
|
await initializedContract.exposed__ImplRoot_init();
|
||||||
|
await initializedContract.connect(deployer).grantRole(SECURITY_ROLE, securityMultisig.address);
|
||||||
|
await initializedContract.connect(deployer).grantRole(OPERATIONS_ROLE, operationsMultisig.address);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should allow critical multisig to grant roles", async () => {
|
||||||
|
await expect(initializedContract.connect(securityMultisig).grantRole(OPERATIONS_ROLE, user1.address)).to.not.be
|
||||||
|
.reverted;
|
||||||
|
|
||||||
|
expect(await initializedContract.hasRole(OPERATIONS_ROLE, user1.address)).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should allow critical multisig to revoke roles", async () => {
|
||||||
|
// First grant a role to user1
|
||||||
|
await initializedContract.connect(securityMultisig).grantRole(OPERATIONS_ROLE, user1.address);
|
||||||
|
expect(await initializedContract.hasRole(OPERATIONS_ROLE, user1.address)).to.be.true;
|
||||||
|
|
||||||
|
// Then revoke it
|
||||||
|
await expect(initializedContract.connect(securityMultisig).revokeRole(OPERATIONS_ROLE, user1.address)).to.not.be
|
||||||
|
.reverted;
|
||||||
|
|
||||||
|
expect(await initializedContract.hasRole(OPERATIONS_ROLE, user1.address)).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should prevent standard multisig from granting critical role", async () => {
|
||||||
|
await expect(
|
||||||
|
initializedContract.connect(operationsMultisig).grantRole(SECURITY_ROLE, user1.address),
|
||||||
|
).to.be.revertedWithCustomError(initializedContract, "AccessControlUnauthorizedAccount");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should prevent unauthorized users from granting roles", async () => {
|
||||||
|
await expect(
|
||||||
|
initializedContract.connect(user1).grantRole(OPERATIONS_ROLE, user1.address),
|
||||||
|
).to.be.revertedWithCustomError(initializedContract, "AccessControlUnauthorizedAccount");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should allow role holders to renounce their own roles", async () => {
|
||||||
|
// Grant role to user1
|
||||||
|
await initializedContract.connect(securityMultisig).grantRole(OPERATIONS_ROLE, user1.address);
|
||||||
|
expect(await initializedContract.hasRole(OPERATIONS_ROLE, user1.address)).to.be.true;
|
||||||
|
|
||||||
|
// User1 can renounce their own role
|
||||||
|
await expect(initializedContract.connect(user1).renounceRole(OPERATIONS_ROLE, user1.address)).to.not.be.reverted;
|
||||||
|
|
||||||
|
expect(await initializedContract.hasRole(OPERATIONS_ROLE, user1.address)).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should prevent users from renouncing others' roles", async () => {
|
||||||
|
await expect(
|
||||||
|
initializedContract.connect(user1).renounceRole(SECURITY_ROLE, securityMultisig.address),
|
||||||
|
).to.be.revertedWithCustomError(initializedContract, "AccessControlBadConfirmation");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Upgrade Authorization", () => {
|
describe("Upgrade Authorization", () => {
|
||||||
let proxy: any;
|
let initializedContract: MockImplRoot;
|
||||||
let implContract: any;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const MockImplRootFactory = await ethers.getContractFactory("MockImplRoot", owner);
|
const MockImplRootFactory = await ethers.getContractFactory("MockImplRoot");
|
||||||
implContract = await MockImplRootFactory.deploy();
|
initializedContract = await MockImplRootFactory.deploy();
|
||||||
await implContract.waitForDeployment();
|
await initializedContract.waitForDeployment();
|
||||||
|
|
||||||
const initData = implContract.interface.encodeFunctionData("exposed__Ownable_init", [owner.address]);
|
// Initialize and transfer roles
|
||||||
|
await initializedContract.exposed__ImplRoot_init();
|
||||||
const ProxyFactory = await ethers.getContractFactory("ERC1967Proxy");
|
await initializedContract.connect(deployer).grantRole(SECURITY_ROLE, securityMultisig.address);
|
||||||
proxy = await ProxyFactory.deploy(implContract.target, initData);
|
await initializedContract.connect(deployer).grantRole(OPERATIONS_ROLE, operationsMultisig.address);
|
||||||
await proxy.waitForDeployment();
|
|
||||||
|
|
||||||
mockImplRoot = await ethers.getContractAt("MockImplRoot", proxy.target);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should revert when calling _authorizeUpgrade from non-proxy", async () => {
|
it("should allow critical multisig to authorize upgrades", async () => {
|
||||||
const MockImplRootFactory = await ethers.getContractFactory("MockImplRoot", owner);
|
const newImplementation = ethers.Wallet.createRandom().address;
|
||||||
const newImpl = await MockImplRootFactory.deploy();
|
|
||||||
await newImpl.waitForDeployment();
|
|
||||||
|
|
||||||
await expect(implContract.exposed_authorizeUpgrade(newImpl.target)).to.be.revertedWithCustomError(
|
// Note: _authorizeUpgrade is internal and can only be called through proxy upgrade mechanism
|
||||||
implContract,
|
// We test this by verifying the critical multisig has the required role
|
||||||
"UUPSUnauthorizedCallContext",
|
expect(await initializedContract.hasRole(SECURITY_ROLE, securityMultisig.address)).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should prevent standard multisig from authorizing upgrades", async () => {
|
||||||
|
const newImplementation = ethers.Wallet.createRandom().address;
|
||||||
|
|
||||||
|
// Standard multisig should not have SECURITY_ROLE
|
||||||
|
expect(await initializedContract.hasRole(SECURITY_ROLE, operationsMultisig.address)).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should prevent unauthorized users from authorizing upgrades", async () => {
|
||||||
|
const newImplementation = ethers.Wallet.createRandom().address;
|
||||||
|
|
||||||
|
// Unauthorized users should not have SECURITY_ROLE
|
||||||
|
expect(await initializedContract.hasRole(SECURITY_ROLE, user1.address)).to.be.false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Role Hierarchy", () => {
|
||||||
|
let initializedContract: MockImplRoot;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const MockImplRootFactory = await ethers.getContractFactory("MockImplRoot");
|
||||||
|
initializedContract = await MockImplRootFactory.deploy();
|
||||||
|
await initializedContract.waitForDeployment();
|
||||||
|
|
||||||
|
await initializedContract.exposed__ImplRoot_init();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have SECURITY_ROLE as admin of both roles", async () => {
|
||||||
|
expect(await initializedContract.getRoleAdmin(SECURITY_ROLE)).to.equal(SECURITY_ROLE);
|
||||||
|
expect(await initializedContract.getRoleAdmin(OPERATIONS_ROLE)).to.equal(SECURITY_ROLE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should allow SECURITY_ROLE holders to manage OPERATIONS_ROLE", async () => {
|
||||||
|
// Grant SECURITY_ROLE to securityMultisig
|
||||||
|
await initializedContract.connect(deployer).grantRole(SECURITY_ROLE, securityMultisig.address);
|
||||||
|
|
||||||
|
// Critical multisig should be able to grant OPERATIONS_ROLE
|
||||||
|
await expect(initializedContract.connect(securityMultisig).grantRole(OPERATIONS_ROLE, user1.address)).to.not.be
|
||||||
|
.reverted;
|
||||||
|
|
||||||
|
expect(await initializedContract.hasRole(OPERATIONS_ROLE, user1.address)).to.be.true;
|
||||||
|
|
||||||
|
// Critical multisig should be able to revoke OPERATIONS_ROLE
|
||||||
|
await expect(initializedContract.connect(securityMultisig).revokeRole(OPERATIONS_ROLE, user1.address)).to.not.be
|
||||||
|
.reverted;
|
||||||
|
|
||||||
|
expect(await initializedContract.hasRole(OPERATIONS_ROLE, user1.address)).to.be.false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Complete Workflow", () => {
|
||||||
|
it("should demonstrate complete deployment and role transfer workflow", async () => {
|
||||||
|
console.log("\n🔄 Starting Complete ImplRoot Workflow");
|
||||||
|
|
||||||
|
// 1. Deploy contract
|
||||||
|
const MockImplRootFactory = await ethers.getContractFactory("MockImplRoot");
|
||||||
|
const contract = await MockImplRootFactory.deploy();
|
||||||
|
await contract.waitForDeployment();
|
||||||
|
|
||||||
|
console.log("✅ Step 1: Contract deployed");
|
||||||
|
|
||||||
|
// 2. Initialize with deployer having roles
|
||||||
|
await contract.exposed__ImplRoot_init();
|
||||||
|
|
||||||
|
console.log("✅ Step 2: Contract initialized");
|
||||||
|
console.log(` - Deployer has SECURITY_ROLE: ${await contract.hasRole(SECURITY_ROLE, deployer.address)}`);
|
||||||
|
console.log(` - Deployer has OPERATIONS_ROLE: ${await contract.hasRole(OPERATIONS_ROLE, deployer.address)}`);
|
||||||
|
|
||||||
|
// 3. Grant roles to multisigs
|
||||||
|
await contract.connect(deployer).grantRole(SECURITY_ROLE, securityMultisig.address);
|
||||||
|
await contract.connect(deployer).grantRole(OPERATIONS_ROLE, operationsMultisig.address);
|
||||||
|
|
||||||
|
console.log("✅ Step 3: Roles granted to multisigs");
|
||||||
|
console.log(
|
||||||
|
` - Critical multisig has SECURITY_ROLE: ${await contract.hasRole(SECURITY_ROLE, securityMultisig.address)}`,
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
` - Standard multisig has OPERATIONS_ROLE: ${await contract.hasRole(OPERATIONS_ROLE, operationsMultisig.address)}`,
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
it("should revert when non-owner calls _authorizeUpgrade", async () => {
|
// 4. Verify multisigs can operate (check role permissions)
|
||||||
const MockImplRootFactory = await ethers.getContractFactory("MockImplRoot", owner);
|
expect(await contract.hasRole(SECURITY_ROLE, securityMultisig.address)).to.be.true;
|
||||||
const newImpl = await MockImplRootFactory.deploy();
|
|
||||||
await newImpl.waitForDeployment();
|
|
||||||
|
|
||||||
await expect(mockImplRoot.connect(user1).exposed_authorizeUpgrade(newImpl.target))
|
console.log("✅ Step 4: Multisigs verified functional");
|
||||||
.to.be.revertedWithCustomError(mockImplRoot, "OwnableUnauthorizedAccount")
|
|
||||||
.withArgs(user1.address);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should allow owner to call _authorizeUpgrade through proxy", async () => {
|
// 5. Renounce deployer roles
|
||||||
const MockImplRootFactory = await ethers.getContractFactory("MockImplRoot", owner);
|
await contract.connect(deployer).renounceRole(SECURITY_ROLE, deployer.address);
|
||||||
const newImpl = await MockImplRootFactory.deploy();
|
await contract.connect(deployer).renounceRole(OPERATIONS_ROLE, deployer.address);
|
||||||
await newImpl.waitForDeployment();
|
|
||||||
|
|
||||||
await expect(mockImplRoot.connect(owner).exposed_authorizeUpgrade(newImpl.target)).to.not.be.reverted;
|
console.log("✅ Step 5: Deployer roles renounced");
|
||||||
|
console.log(` - Deployer has SECURITY_ROLE: ${await contract.hasRole(SECURITY_ROLE, deployer.address)}`);
|
||||||
|
console.log(` - Deployer has OPERATIONS_ROLE: ${await contract.hasRole(OPERATIONS_ROLE, deployer.address)}`);
|
||||||
|
|
||||||
|
// 6. Final verification
|
||||||
|
expect(await contract.hasRole(SECURITY_ROLE, securityMultisig.address)).to.be.true;
|
||||||
|
expect(await contract.hasRole(OPERATIONS_ROLE, operationsMultisig.address)).to.be.true;
|
||||||
|
expect(await contract.hasRole(SECURITY_ROLE, deployer.address)).to.be.false;
|
||||||
|
expect(await contract.hasRole(OPERATIONS_ROLE, deployer.address)).to.be.false;
|
||||||
|
|
||||||
|
console.log("🎉 Complete ImplRoot workflow successful!");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,9 +8,14 @@ describe("PCR0Manager", function () {
|
|||||||
let owner: SignerWithAddress;
|
let owner: SignerWithAddress;
|
||||||
let other: SignerWithAddress;
|
let other: SignerWithAddress;
|
||||||
|
|
||||||
// Sample PCR0 value for testing (48 bytes)
|
// Sample PCR0 value for testing
|
||||||
const samplePCR0 = "0x" + "00".repeat(48);
|
// addPCR0/removePCR0 expect 32 bytes (GCP image hash)
|
||||||
const invalidPCR0 = "0x" + "00".repeat(32); // 32 bytes (invalid size)
|
const samplePCR0_32bytes = "0x" + "ab".repeat(32);
|
||||||
|
// isPCR0Set expects 48 bytes (16 zero bytes prefix + 32 byte hash, for mobile compatibility)
|
||||||
|
const samplePCR0_48bytes = "0x" + "00".repeat(16) + "ab".repeat(32);
|
||||||
|
// Invalid sizes for testing error cases
|
||||||
|
const invalidPCR0_for_add = "0x" + "00".repeat(48); // 48 bytes - invalid for add/remove
|
||||||
|
const invalidPCR0_for_check = "0x" + "00".repeat(32); // 32 bytes - invalid for isPCR0Set
|
||||||
|
|
||||||
beforeEach(async function () {
|
beforeEach(async function () {
|
||||||
[owner, other] = await ethers.getSigners();
|
[owner, other] = await ethers.getSigners();
|
||||||
@@ -21,74 +26,68 @@ describe("PCR0Manager", function () {
|
|||||||
|
|
||||||
describe("addPCR0", function () {
|
describe("addPCR0", function () {
|
||||||
it("should allow owner to add PCR0 value", async function () {
|
it("should allow owner to add PCR0 value", async function () {
|
||||||
await expect(pcr0Manager.addPCR0(samplePCR0)).to.emit(pcr0Manager, "PCR0Added");
|
await expect(pcr0Manager.addPCR0(samplePCR0_32bytes)).to.emit(pcr0Manager, "PCR0Added");
|
||||||
|
|
||||||
expect(await pcr0Manager.isPCR0Set(samplePCR0)).to.be.true;
|
expect(await pcr0Manager.isPCR0Set(samplePCR0_48bytes)).to.be.true;
|
||||||
});
|
|
||||||
|
|
||||||
it("should allow owner to add PCR0 value", async function () {
|
|
||||||
await expect(pcr0Manager.addPCR0(samplePCR0)).to.emit(pcr0Manager, "PCR0Added");
|
|
||||||
|
|
||||||
expect(await pcr0Manager.isPCR0Set(samplePCR0)).to.be.true;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not allow non-owner to add PCR0 value", async function () {
|
it("should not allow non-owner to add PCR0 value", async function () {
|
||||||
await expect(pcr0Manager.connect(other).addPCR0(samplePCR0))
|
await expect(pcr0Manager.connect(other).addPCR0(samplePCR0_32bytes))
|
||||||
.to.be.revertedWithCustomError(pcr0Manager, "OwnableUnauthorizedAccount")
|
.to.be.revertedWithCustomError(pcr0Manager, "AccessControlUnauthorizedAccount")
|
||||||
.withArgs(other.address);
|
.withArgs(other.address, await pcr0Manager.SECURITY_ROLE());
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not allow adding PCR0 with invalid size", async function () {
|
it("should not allow adding PCR0 with invalid size", async function () {
|
||||||
await expect(pcr0Manager.addPCR0(invalidPCR0)).to.be.revertedWith("PCR0 must be 48 bytes");
|
await expect(pcr0Manager.addPCR0(invalidPCR0_for_add)).to.be.revertedWith("PCR0 must be 32 bytes");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not allow adding duplicate PCR0", async function () {
|
it("should not allow adding duplicate PCR0", async function () {
|
||||||
await pcr0Manager.addPCR0(samplePCR0);
|
await pcr0Manager.addPCR0(samplePCR0_32bytes);
|
||||||
await expect(pcr0Manager.addPCR0(samplePCR0)).to.be.revertedWith("PCR0 already set");
|
await expect(pcr0Manager.addPCR0(samplePCR0_32bytes)).to.be.revertedWith("PCR0 already set");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("removePCR0", function () {
|
describe("removePCR0", function () {
|
||||||
beforeEach(async function () {
|
beforeEach(async function () {
|
||||||
await pcr0Manager.addPCR0(samplePCR0);
|
await pcr0Manager.addPCR0(samplePCR0_32bytes);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should allow owner to remove PCR0 value", async function () {
|
it("should allow owner to remove PCR0 value", async function () {
|
||||||
await expect(pcr0Manager.removePCR0(samplePCR0)).to.emit(pcr0Manager, "PCR0Removed");
|
await expect(pcr0Manager.removePCR0(samplePCR0_32bytes)).to.emit(pcr0Manager, "PCR0Removed");
|
||||||
|
|
||||||
expect(await pcr0Manager.isPCR0Set(samplePCR0)).to.be.false;
|
expect(await pcr0Manager.isPCR0Set(samplePCR0_48bytes)).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
||||||
// This is not actually needed, just for increase the coverage of the test code
|
// This is not actually needed, just for increase the coverage of the test code
|
||||||
it("should not allow remove PCR0 with invalid size", async function () {
|
it("should not allow remove PCR0 with invalid size", async function () {
|
||||||
await expect(pcr0Manager.removePCR0(invalidPCR0)).to.be.revertedWith("PCR0 must be 48 bytes");
|
await expect(pcr0Manager.removePCR0(invalidPCR0_for_add)).to.be.revertedWith("PCR0 must be 32 bytes");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not allow non-owner to remove PCR0 value", async function () {
|
it("should not allow non-owner to remove PCR0 value", async function () {
|
||||||
await expect(pcr0Manager.connect(other).removePCR0(samplePCR0))
|
await expect(pcr0Manager.connect(other).removePCR0(samplePCR0_32bytes))
|
||||||
.to.be.revertedWithCustomError(pcr0Manager, "OwnableUnauthorizedAccount")
|
.to.be.revertedWithCustomError(pcr0Manager, "AccessControlUnauthorizedAccount")
|
||||||
.withArgs(other.address);
|
.withArgs(other.address, await pcr0Manager.SECURITY_ROLE());
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not allow removing non-existent PCR0", async function () {
|
it("should not allow removing non-existent PCR0", async function () {
|
||||||
const otherPCR0 = "0x" + "11".repeat(48);
|
const otherPCR0 = "0x" + "11".repeat(32);
|
||||||
await expect(pcr0Manager.removePCR0(otherPCR0)).to.be.revertedWith("PCR0 not set");
|
await expect(pcr0Manager.removePCR0(otherPCR0)).to.be.revertedWith("PCR0 not set");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("isPCR0Set", function () {
|
describe("isPCR0Set", function () {
|
||||||
it("should correctly return PCR0 status", async function () {
|
it("should correctly return PCR0 status", async function () {
|
||||||
expect(await pcr0Manager.isPCR0Set(samplePCR0)).to.be.false;
|
expect(await pcr0Manager.isPCR0Set(samplePCR0_48bytes)).to.be.false;
|
||||||
|
|
||||||
await pcr0Manager.addPCR0(samplePCR0);
|
await pcr0Manager.addPCR0(samplePCR0_32bytes);
|
||||||
expect(await pcr0Manager.isPCR0Set(samplePCR0)).to.be.true;
|
expect(await pcr0Manager.isPCR0Set(samplePCR0_48bytes)).to.be.true;
|
||||||
|
|
||||||
await pcr0Manager.removePCR0(samplePCR0);
|
await pcr0Manager.removePCR0(samplePCR0_32bytes);
|
||||||
expect(await pcr0Manager.isPCR0Set(samplePCR0)).to.be.false;
|
expect(await pcr0Manager.isPCR0Set(samplePCR0_48bytes)).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not allow checking PCR0 with invalid size", async function () {
|
it("should not allow checking PCR0 with invalid size", async function () {
|
||||||
await expect(pcr0Manager.isPCR0Set(invalidPCR0)).to.be.revertedWith("PCR0 must be 48 bytes");
|
await expect(pcr0Manager.isPCR0Set(invalidPCR0_for_check)).to.be.revertedWith("PCR0 must be 48 bytes");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -149,9 +149,11 @@ describe("Aadhaar Registration test", function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should not fail if timestamp is within 20 minutes", async () => {
|
it("should not fail if timestamp is within 20 minutes", async () => {
|
||||||
// Fix the AADHAAR_REGISTRATION_WINDOW that was incorrectly set to 0
|
|
||||||
await deployedActors.hub.setAadhaarRegistrationWindow(20);
|
await deployedActors.hub.setAadhaarRegistrationWindow(20);
|
||||||
|
|
||||||
|
const latestBlock = await ethers.provider.getBlock("latest");
|
||||||
|
const blockTimestamp = latestBlock!.timestamp;
|
||||||
|
|
||||||
const newAadhaarData = prepareAadhaarRegisterTestData(
|
const newAadhaarData = prepareAadhaarRegisterTestData(
|
||||||
privateKeyPem,
|
privateKeyPem,
|
||||||
pubkeyPem,
|
pubkeyPem,
|
||||||
@@ -161,8 +163,7 @@ describe("Aadhaar Registration test", function () {
|
|||||||
"M",
|
"M",
|
||||||
"110051",
|
"110051",
|
||||||
"WB",
|
"WB",
|
||||||
//timestamp 10 minutes ago and converted to timestamp string
|
(blockTimestamp - 10 * 60).toString(),
|
||||||
new Date(Date.now() - 10 * 60 * 1000).getTime().toString(),
|
|
||||||
);
|
);
|
||||||
const newRegisterProof = await generateRegisterAadhaarProof(registerSecret, newAadhaarData.inputs);
|
const newRegisterProof = await generateRegisterAadhaarProof(registerSecret, newAadhaarData.inputs);
|
||||||
|
|
||||||
@@ -171,9 +172,11 @@ describe("Aadhaar Registration test", function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should fail with InvalidUidaiTimestamp when UIDAI timestamp is not within 20 minutes of current time", async () => {
|
it("should fail with InvalidUidaiTimestamp when UIDAI timestamp is not within 20 minutes of current time", async () => {
|
||||||
// Fix the AADHAAR_REGISTRATION_WINDOW that was incorrectly set to 0
|
|
||||||
await deployedActors.hub.setAadhaarRegistrationWindow(20);
|
await deployedActors.hub.setAadhaarRegistrationWindow(20);
|
||||||
|
|
||||||
|
const latestBlock = await ethers.provider.getBlock("latest");
|
||||||
|
const blockTimestamp = latestBlock!.timestamp;
|
||||||
|
|
||||||
const newAadhaarData = prepareAadhaarRegisterTestData(
|
const newAadhaarData = prepareAadhaarRegisterTestData(
|
||||||
privateKeyPem,
|
privateKeyPem,
|
||||||
pubkeyPem,
|
pubkeyPem,
|
||||||
@@ -183,8 +186,7 @@ describe("Aadhaar Registration test", function () {
|
|||||||
"M",
|
"M",
|
||||||
"110051",
|
"110051",
|
||||||
"WB",
|
"WB",
|
||||||
//timestamp 30 minutes ago and converted to timestamp string
|
(blockTimestamp - 30 * 60).toString(),
|
||||||
new Date(Date.now() - 30 * 60 * 1000).getTime().toString(),
|
|
||||||
);
|
);
|
||||||
const newRegisterProof = await generateRegisterAadhaarProof(registerSecret, newAadhaarData.inputs);
|
const newRegisterProof = await generateRegisterAadhaarProof(registerSecret, newAadhaarData.inputs);
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,6 @@
|
|||||||
"@babel/core": "^7.28.4",
|
"@babel/core": "^7.28.4",
|
||||||
"@babel/runtime": "^7.28.4",
|
"@babel/runtime": "^7.28.4",
|
||||||
"@noble/curves": "1.9.7",
|
"@noble/curves": "1.9.7",
|
||||||
"@noble/hashes": "1.8.0",
|
|
||||||
"@swc/core": "1.7.36",
|
"@swc/core": "1.7.36",
|
||||||
"@tamagui/animations-react-native": "1.126.14",
|
"@tamagui/animations-react-native": "1.126.14",
|
||||||
"@tamagui/toast": "1.126.14",
|
"@tamagui/toast": "1.126.14",
|
||||||
|
|||||||
@@ -151,7 +151,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.28.3",
|
"@babel/runtime": "^7.28.3",
|
||||||
"@selfxyz/common": "workspace:^",
|
"@selfxyz/common": "workspace:^",
|
||||||
"@selfxyz/euclid": "^0.4.1",
|
"@selfxyz/euclid": "^0.6.0",
|
||||||
"@xstate/react": "^5.0.5",
|
"@xstate/react": "^5.0.5",
|
||||||
"node-forge": "^1.3.1",
|
"node-forge": "^1.3.1",
|
||||||
"react-native-nfc-manager": "^3.17.1",
|
"react-native-nfc-manager": "^3.17.1",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const BRANCH = 'main';
|
|||||||
// Environment detection
|
// Environment detection
|
||||||
const isCI = process.env.CI === 'true';
|
const isCI = process.env.CI === 'true';
|
||||||
const repoToken = process.env.SELFXYZ_INTERNAL_REPO_PAT;
|
const repoToken = process.env.SELFXYZ_INTERNAL_REPO_PAT;
|
||||||
|
const appToken = process.env.SELFXYZ_APP_TOKEN; // GitHub App installation token
|
||||||
const isDryRun = process.env.DRY_RUN === 'true';
|
const isDryRun = process.env.DRY_RUN === 'true';
|
||||||
|
|
||||||
function log(message, type = 'info') {
|
function log(message, type = 'info') {
|
||||||
@@ -89,19 +90,24 @@ function setupSubmodule() {
|
|||||||
|
|
||||||
let submoduleUrl;
|
let submoduleUrl;
|
||||||
|
|
||||||
if (isCI && repoToken) {
|
if (isCI && appToken) {
|
||||||
|
// CI environment with GitHub App installation token
|
||||||
|
// Security: NEVER embed credentials in git URLs. Rely on CI-provided auth via:
|
||||||
|
// - ~/.netrc, a Git credential helper, or SSH agent configuration.
|
||||||
|
submoduleUrl = `https://github.com/${GITHUB_ORG}/${REPO_NAME}.git`;
|
||||||
|
} else if (isCI && repoToken) {
|
||||||
// CI environment with Personal Access Token
|
// CI environment with Personal Access Token
|
||||||
log('CI detected: Using SELFXYZ_INTERNAL_REPO_PAT for submodule', 'info');
|
// Security: NEVER embed credentials in git URLs. Rely on CI-provided auth via:
|
||||||
submoduleUrl = `https://${repoToken}@github.com/${GITHUB_ORG}/${REPO_NAME}.git`;
|
// - ~/.netrc, a Git credential helper, or SSH agent configuration.
|
||||||
|
submoduleUrl = `https://github.com/${GITHUB_ORG}/${REPO_NAME}.git`;
|
||||||
} else if (isCI) {
|
} else if (isCI) {
|
||||||
log('CI environment detected but SELFXYZ_INTERNAL_REPO_PAT not available - skipping private module setup', 'info');
|
log('CI environment detected but no token available - skipping private module setup', 'info');
|
||||||
log('This is expected for forked PRs or environments without access to private modules', 'info');
|
log('This is expected for forked PRs or environments without access to private modules', 'info');
|
||||||
return false; // Return false to indicate setup was skipped
|
return false; // Return false to indicate setup was skipped
|
||||||
} else if (usingHTTPSGitAuth()) {
|
} else if (usingHTTPSGitAuth()) {
|
||||||
submoduleUrl = `https://github.com/${GITHUB_ORG}/${REPO_NAME}.git`;
|
submoduleUrl = `https://github.com/${GITHUB_ORG}/${REPO_NAME}.git`;
|
||||||
} else {
|
} else {
|
||||||
// Local development with SSH
|
// Local development with SSH
|
||||||
log('Local development: Using SSH for submodule', 'info');
|
|
||||||
submoduleUrl = `git@github.com:${GITHUB_ORG}/${REPO_NAME}.git`;
|
submoduleUrl = `git@github.com:${GITHUB_ORG}/${REPO_NAME}.git`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +119,7 @@ function setupSubmodule() {
|
|||||||
} else {
|
} else {
|
||||||
// Add submodule
|
// Add submodule
|
||||||
const addCommand = `git submodule add -b ${BRANCH} "${submoduleUrl}" mobile-sdk-native`;
|
const addCommand = `git submodule add -b ${BRANCH} "${submoduleUrl}" mobile-sdk-native`;
|
||||||
if (isCI && repoToken) {
|
if (isCI && (appToken || repoToken)) {
|
||||||
// Security: Run command silently to avoid token exposure in logs
|
// Security: Run command silently to avoid token exposure in logs
|
||||||
runCommand(addCommand, { stdio: 'pipe' });
|
runCommand(addCommand, { stdio: 'pipe' });
|
||||||
} else {
|
} else {
|
||||||
@@ -125,7 +131,7 @@ function setupSubmodule() {
|
|||||||
return true; // Return true to indicate successful setup
|
return true; // Return true to indicate successful setup
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isCI) {
|
if (isCI) {
|
||||||
log('Submodule setup failed in CI environment. Check SELFXYZ_INTERNAL_REPO_PAT permissions.', 'error');
|
log('Submodule setup failed in CI environment. Check repository access/credentials configuration.', 'error');
|
||||||
} else {
|
} else {
|
||||||
log('Submodule setup failed. Ensure you have SSH access to the repository.', 'error');
|
log('Submodule setup failed. Ensure you have SSH access to the repository.', 'error');
|
||||||
}
|
}
|
||||||
@@ -169,7 +175,7 @@ function setupMobileSDKNative() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Security: Remove credential-embedded remote URL after setup
|
// Security: Remove credential-embedded remote URL after setup
|
||||||
if (isCI && repoToken && !isDryRun) {
|
if (isCI && (appToken || repoToken) && !isDryRun) {
|
||||||
scrubGitRemoteUrl();
|
scrubGitRemoteUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ export const BackupEvents = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentEvents = {
|
export const DocumentEvents = {
|
||||||
|
COUNTRY_HELP_TAPPED: 'Document: Country Help Tapped',
|
||||||
ADD_NEW_AADHAAR_SELECTED: 'Document: Add Aadhaar',
|
ADD_NEW_AADHAAR_SELECTED: 'Document: Add Aadhaar',
|
||||||
ADD_NEW_MOCK_SELECTED: 'Document: Add New Document via Mock',
|
ADD_NEW_MOCK_SELECTED: 'Document: Add New Document via Mock',
|
||||||
ADD_NEW_SCAN_SELECTED: 'Document: Add New Document via Scan',
|
ADD_NEW_SCAN_SELECTED: 'Document: Add New Document via Scan',
|
||||||
|
|||||||
@@ -5,15 +5,18 @@
|
|||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
import { commonNames } from '@selfxyz/common/constants/countries';
|
import { commonNames } from '@selfxyz/common/constants/countries';
|
||||||
import { CountryPickerScreen as CountryPickerUI } from '@selfxyz/euclid';
|
import { CountryPickerScreen as CountryPickerUI, type SafeArea } from '@selfxyz/euclid';
|
||||||
|
|
||||||
import { RoundFlag } from '../../components';
|
import { RoundFlag } from '../../components';
|
||||||
|
import { DocumentEvents } from '../../constants/analytics';
|
||||||
import { useSelfClient } from '../../context';
|
import { useSelfClient } from '../../context';
|
||||||
import { useCountries } from '../../documents/useCountries';
|
import { useCountries } from '../../documents/useCountries';
|
||||||
import { buttonTap } from '../../haptic';
|
import { buttonTap } from '../../haptic';
|
||||||
import { SdkEvents } from '../../types/events';
|
import { SdkEvents } from '../../types/events';
|
||||||
|
|
||||||
const CountryPickerScreen: React.FC = () => {
|
const CountryPickerScreen: React.FC<SafeArea> & { statusBar: typeof CountryPickerUI.statusBar } = ({
|
||||||
|
insets,
|
||||||
|
}: SafeArea) => {
|
||||||
const selfClient = useSelfClient();
|
const selfClient = useSelfClient();
|
||||||
|
|
||||||
const [searchValue, setSearchValue] = useState('');
|
const [searchValue, setSearchValue] = useState('');
|
||||||
@@ -57,9 +60,9 @@ const CountryPickerScreen: React.FC = () => {
|
|||||||
const onSearchChange = useCallback((value: string) => {
|
const onSearchChange = useCallback((value: string) => {
|
||||||
setSearchValue(value);
|
setSearchValue(value);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CountryPickerUI
|
<CountryPickerUI
|
||||||
|
insets={insets}
|
||||||
isLoading={loading}
|
isLoading={loading}
|
||||||
countries={countryList}
|
countries={countryList}
|
||||||
onCountrySelect={onCountrySelect}
|
onCountrySelect={onCountrySelect}
|
||||||
@@ -69,11 +72,11 @@ const CountryPickerScreen: React.FC = () => {
|
|||||||
getCountryName={getCountryName}
|
getCountryName={getCountryName}
|
||||||
searchValue={searchValue}
|
searchValue={searchValue}
|
||||||
onClose={selfClient.goBack}
|
onClose={selfClient.goBack}
|
||||||
onInfoPress={() => console.log('Info pressed TODO: Implement')}
|
onInfoPress={() => selfClient.trackEvent(DocumentEvents.COUNTRY_HELP_TAPPED)}
|
||||||
onSearchChange={onSearchChange}
|
onSearchChange={onSearchChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
CountryPickerScreen.displayName = 'CountryPickerScreen';
|
CountryPickerScreen.displayName = 'CountryPickerScreen';
|
||||||
|
CountryPickerScreen.statusBar = CountryPickerUI.statusBar;
|
||||||
export default CountryPickerScreen;
|
export default CountryPickerScreen;
|
||||||
|
|||||||
@@ -46,12 +46,15 @@ target "SelfDemoApp" do
|
|||||||
nfc_repo_url = if !is_selfxyz_repo
|
nfc_repo_url = if !is_selfxyz_repo
|
||||||
puts "📦 Using public NFCPassportReader for external fork (#{ENV["GITHUB_REPOSITORY"]})"
|
puts "📦 Using public NFCPassportReader for external fork (#{ENV["GITHUB_REPOSITORY"]})"
|
||||||
"https://github.com/PLACEHOLDER/NFCPassportReader.git"
|
"https://github.com/PLACEHOLDER/NFCPassportReader.git"
|
||||||
elsif ENV["GITHUB_ACTIONS"] == "true" && ENV["SELFXYZ_INTERNAL_REPO_PAT"] && !ENV["SELFXYZ_INTERNAL_REPO_PAT"].empty?
|
elsif ENV["GITHUB_ACTIONS"] == "true"
|
||||||
puts "📦 Using private NFCPassportReader with PAT (selfxyz GitHub Actions)"
|
# CI: NEVER embed credentials in URLs. Rely on workflow-provided auth via:
|
||||||
"https://#{ENV["SELFXYZ_INTERNAL_REPO_PAT"]}@github.com/selfxyz/NFCPassportReader.git"
|
# - ~/.netrc or a Git credential helper, and token masking in logs.
|
||||||
|
"https://github.com/selfxyz/NFCPassportReader.git"
|
||||||
elsif using_https_git_auth?
|
elsif using_https_git_auth?
|
||||||
|
# Local development with HTTPS GitHub auth via gh - use HTTPS to private repo
|
||||||
"https://github.com/selfxyz/NFCPassportReader.git"
|
"https://github.com/selfxyz/NFCPassportReader.git"
|
||||||
else
|
else
|
||||||
|
# Local development in selfxyz repo - use SSH to private repo
|
||||||
puts "📦 Using SSH for private NFCPassportReader (local selfxyz development)"
|
puts "📦 Using SSH for private NFCPassportReader (local selfxyz development)"
|
||||||
"git@github.com:selfxyz/NFCPassportReader.git"
|
"git@github.com:selfxyz/NFCPassportReader.git"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import ScreenLayout from '../components/ScreenLayout';
|
|||||||
export default function CountrySelection({ onBack }: { onBack: () => void }) {
|
export default function CountrySelection({ onBack }: { onBack: () => void }) {
|
||||||
return (
|
return (
|
||||||
<ScreenLayout title="GETTING STARTED" onBack={onBack}>
|
<ScreenLayout title="GETTING STARTED" onBack={onBack}>
|
||||||
<SDKCountryPickerScreen />
|
<SDKCountryPickerScreen insets={{ top: 0, bottom: 0 }} />
|
||||||
</ScreenLayout>
|
</ScreenLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user