diff --git a/.eslintrc.js b/.eslintrc.js index 147bbca7708..58a60686452 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -9,8 +9,6 @@ const utilNumberImportBurndownFiles = [ 'app/component-library/components-temp/CustomSpendCap/CustomInput/CustomInput.tsx', 'app/component-library/components-temp/CustomSpendCap/CustomSpendCap.tsx', - 'app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.tsx', - 'app/component-library/components-temp/Price/AggregatedPercentage/utils.ts', 'app/components/UI/AccountInfoCard/index.js', 'app/components/UI/AssetOverview/Price/Price.advanced.tsx', 'app/components/UI/AssetOverview/Price/Price.legacy.tsx', diff --git a/.github/actions/check-force-builds/action.yml b/.github/actions/check-force-builds/action.yml new file mode 100644 index 00000000000..cead62e05db --- /dev/null +++ b/.github/actions/check-force-builds/action.yml @@ -0,0 +1,81 @@ +name: 'Check force-builds override' +description: >- + Detects whether the current workflow run should bypass native build reuse + (both the GHA cache and the cross-run artifact lookup) and always compile + fresh. The override is honored on `pull_request` events via a `force-builds` + label OR a `[force-builds]` token in the head commit message. It is + intentionally ignored on `merge_group` and `push` events so the merge queue + always uses hash-verified reuse. + +inputs: + github-token: + description: >- + GitHub token with `pull-requests: read` (for label lookup) and + `contents: read` (to fetch the head commit message via the REST API). + required: true + label-name: + description: 'PR label that, when present, forces fresh builds' + required: false + default: 'force-builds' + commit-tag: + description: 'Case-sensitive substring in the head commit message that forces fresh builds' + required: false + default: '[force-builds]' + +outputs: + force: + description: "'true' when fresh builds should be forced, otherwise 'false'" + value: ${{ steps.compute.outputs.force }} + +runs: + using: 'composite' + steps: + - name: Compute force-builds flag + id: compute + shell: bash + env: + GH_TOKEN: ${{ inputs.github-token }} + LABEL_NAME: ${{ inputs.label-name }} + COMMIT_TAG: ${{ inputs.commit-tag }} + EVENT_NAME: ${{ github.event_name }} + HEAD_COMMIT_HASH: ${{ github.event.pull_request.head.sha }} + PR_NUMBER: ${{ github.event.pull_request.number }} + REPOSITORY: ${{ github.repository }} + run: | + FORCE="false" + + if [[ "$EVENT_NAME" != "pull_request" ]]; then + echo "Event is $EVENT_NAME; force-builds override is ignored outside pull_request events." + echo "force=$FORCE" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # Commit-message tag. + COMMIT_MESSAGE="" + if COMMIT_MESSAGE=$(gh api \ + "repos/$REPOSITORY/commits/$HEAD_COMMIT_HASH" \ + --jq '.commit.message' 2>/dev/null); then + if printf '%s' "$COMMIT_MESSAGE" \ + | grep --fixed-strings --quiet "$COMMIT_TAG"; then + echo "-> force=true because '$COMMIT_TAG' was found in commit message of $HEAD_COMMIT_HASH" + FORCE="true" + fi + else + echo "::warning::Failed to fetch commit message for $HEAD_COMMIT_HASH via GitHub API; commit-tag force-builds check skipped for this run (the '$LABEL_NAME' label path still works)." + fi + + # PR label + if [[ -n "$PR_NUMBER" ]]; then + if gh pr view "$PR_NUMBER" --repo "$REPOSITORY" \ + --json labels --jq '.labels[].name' \ + | grep --fixed-strings --line-regexp --quiet "$LABEL_NAME"; then + echo "-> force=true because '$LABEL_NAME' label is applied to PR #$PR_NUMBER" + FORCE="true" + fi + fi + + if [[ "$FORCE" == "false" ]]; then + echo "No force-builds override active." + fi + + echo "force=$FORCE" >> "$GITHUB_OUTPUT" diff --git a/.github/actions/find-reusable-build/action.yml b/.github/actions/find-reusable-build/action.yml new file mode 100644 index 00000000000..fe0b7095deb --- /dev/null +++ b/.github/actions/find-reusable-build/action.yml @@ -0,0 +1,255 @@ +name: 'Find reusable build from prior run' +description: >- + Searches recent workflow runs across three tiers (same branch, base branch, + then any open PR branch) for a run whose `build-source-hash` commit status + matches the current fingerprint AND whose required build artifacts are still + available. If a match is found, outputs the run id so a subsequent + `actions/download-artifact` step can pull the artifacts directly instead of + triggering a fresh native build. + + The third (cross-PR) tier is required because GitHub's `listWorkflowRuns` + `branch` parameter filters against `head_branch` — the PR source branch for + `pull_request` events — so branch-scoped lookups can never discover other + PRs' runs. The cross-PR tier drops the branch filter and instead uses + `event: pull_request` to let the fingerprint itself act as the cross-PR + deduplication key. + +inputs: + fingerprint: + description: 'The @expo/fingerprint hash the candidate must match' + required: true + artifact-names: + description: 'JSON array of artifact names that must all be present on the candidate run' + required: true + github-token: + description: 'GitHub token with `actions: read` and `statuses: read` permissions' + required: true + workflow-file: + description: 'Workflow filename whose runs will be searched' + required: false + default: 'ci.yml' + base-branch: + description: 'Fallback branch when no same-branch match is found' + required: false + default: 'main' + status-context: + description: 'Commit status context that carries the fingerprint' + required: false + default: 'build-source-hash' + max-candidates-per-branch: + description: 'How many recent runs to inspect per branch-scoped tier (same-branch, base-branch)' + required: false + default: '10' + max-candidates-cross-pr: + description: >- + How many recent `pull_request`-event runs (across all branches) to inspect + in the cross-PR tier. The fingerprint filter is highly discriminating, so + the practical cost is one `getCombinedStatusForRef` call per candidate + until a match is found. + required: false + default: '30' + +outputs: + found: + description: "'true' when a reusable run was found" + value: ${{ steps.lookup.outputs.found }} + run-id: + description: 'Workflow run id that produced the reusable artifacts' + value: ${{ steps.lookup.outputs.run-id }} + source-sha: + description: 'Commit SHA of the reusable run' + value: ${{ steps.lookup.outputs.source-sha }} + source-branch: + description: 'Branch of the reusable run (same-branch or base-branch)' + value: ${{ steps.lookup.outputs.source-branch }} + +runs: + using: 'composite' + steps: + - name: Search prior runs for matching fingerprint + id: lookup + uses: actions/github-script@v7 + continue-on-error: true + env: + TARGET_FINGERPRINT: ${{ inputs.fingerprint }} + ARTIFACT_NAMES_JSON: ${{ inputs.artifact-names }} + WORKFLOW_FILE: ${{ inputs.workflow-file }} + BASE_BRANCH: ${{ inputs.base-branch }} + STATUS_CONTEXT: ${{ inputs.status-context }} + MAX_CANDIDATES: ${{ inputs.max-candidates-per-branch }} + MAX_CANDIDATES_CROSS_PR: ${{ inputs.max-candidates-cross-pr }} + HEAD_BRANCH: ${{ github.head_ref || github.ref_name }} + HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + CURRENT_RUN_ID: ${{ github.run_id }} + with: + github-token: ${{ inputs.github-token }} + script: | + const { + TARGET_FINGERPRINT, + ARTIFACT_NAMES_JSON, + WORKFLOW_FILE, + BASE_BRANCH, + STATUS_CONTEXT, + MAX_CANDIDATES, + MAX_CANDIDATES_CROSS_PR, + HEAD_BRANCH, + HEAD_SHA, + CURRENT_RUN_ID, + } = process.env; + + const setNotFound = () => { + core.setOutput('found', 'false'); + core.setOutput('run-id', ''); + core.setOutput('source-sha', ''); + core.setOutput('source-branch', ''); + }; + + if (!TARGET_FINGERPRINT) { + core.warning('No fingerprint provided; skipping lookup'); + setNotFound(); + return; + } + + let requiredArtifacts; + try { + requiredArtifacts = JSON.parse(ARTIFACT_NAMES_JSON); + } catch (err) { + core.warning(`Could not parse artifact-names input: ${err.message}`); + setNotFound(); + return; + } + if (!Array.isArray(requiredArtifacts) || requiredArtifacts.length === 0) { + core.warning('artifact-names must be a non-empty JSON array'); + setNotFound(); + return; + } + + const maxCandidates = Number(MAX_CANDIDATES) || 10; + const maxCandidatesCrossPr = Number(MAX_CANDIDATES_CROSS_PR) || 30; + const currentRunId = String(CURRENT_RUN_ID); + + // Three-tier discovery: + // 1. same-branch — fastest path, catches retries and new commits + // on the current PR. + // 2. base-branch — catches post-merge CI runs on `main`. Only + // matches `push`-event runs (pull_request runs + // have head_branch=, not main). + // 3. cross-pr — searches recent `pull_request` runs across + // ALL source branches so two unrelated PRs with + // the same fingerprint can reuse each other's + // artifacts. This tier deliberately drops the + // `branch` filter; without it, branch-scoped + // lookups can never discover another PR's run + // (GitHub filters `branch` against head_branch, + // which is the PR source branch). + const tiers = [ + { + label: `same-branch (branch=${HEAD_BRANCH})`, + params: { branch: HEAD_BRANCH, per_page: maxCandidates }, + }, + ]; + if (BASE_BRANCH && BASE_BRANCH !== HEAD_BRANCH) { + tiers.push({ + label: `base-branch (branch=${BASE_BRANCH})`, + params: { branch: BASE_BRANCH, per_page: maxCandidates }, + }); + } + tiers.push({ + label: `cross-pr (event=pull_request, any branch, last ${maxCandidatesCrossPr} runs)`, + params: { event: 'pull_request', per_page: maxCandidatesCrossPr }, + // Skip runs already visited by the same-branch tier to avoid + // wasting API calls on duplicates. + skipHeadBranch: HEAD_BRANCH, + }); + + async function getFingerprintForSha(sha) { + try { + const { data } = await github.rest.repos.getCombinedStatusForRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: sha, + per_page: 100, + }); + const status = data.statuses.find((s) => s.context === STATUS_CONTEXT); + return status ? status.description : null; + } catch (err) { + core.info(`getCombinedStatusForRef failed for ${sha}: ${err.message}`); + return null; + } + } + + async function hasAllArtifacts(runId) { + try { + const artifacts = await github.paginate( + github.rest.actions.listWorkflowRunArtifacts, + { + owner: context.repo.owner, + repo: context.repo.repo, + run_id: runId, + per_page: 100, + }, + ); + const available = new Set( + artifacts + .filter((a) => !a.expired) + .map((a) => a.name), + ); + const missing = requiredArtifacts.filter((n) => !available.has(n)); + if (missing.length > 0) { + core.info(`Run ${runId} missing artifacts: ${missing.join(', ')}`); + return false; + } + return true; + } catch (err) { + core.info(`listWorkflowRunArtifacts failed for ${runId}: ${err.message}`); + return false; + } + } + + const seenRunIds = new Set(); + seenRunIds.add(currentRunId); + + for (const tier of tiers) { + core.info(`Searching tier: ${tier.label}`); + let runs; + try { + const { data } = await github.rest.actions.listWorkflowRuns({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: WORKFLOW_FILE, + ...tier.params, + }); + runs = data.workflow_runs || []; + } catch (err) { + core.warning(`listWorkflowRuns failed for tier "${tier.label}": ${err.message}`); + continue; + } + + for (const run of runs) { + const runIdStr = String(run.id); + if (seenRunIds.has(runIdStr)) continue; + seenRunIds.add(runIdStr); + + if (tier.skipHeadBranch && run.head_branch === tier.skipHeadBranch) continue; + + if (run.status !== 'completed' && run.status !== 'in_progress') continue; + + const fingerprint = await getFingerprintForSha(run.head_sha); + if (!fingerprint) continue; + if (fingerprint !== TARGET_FINGERPRINT) continue; + + if (!(await hasAllArtifacts(run.id))) continue; + + core.info( + `Match: tier="${tier.label}" run=${run.id} sha=${run.head_sha} branch=${run.head_branch} url=${run.html_url}`, + ); + core.setOutput('found', 'true'); + core.setOutput('run-id', runIdStr); + core.setOutput('source-sha', run.head_sha); + core.setOutput('source-branch', run.head_branch || ''); + return; + } + } + + core.info('No reusable build found across any tier'); + setNotFound(); diff --git a/.github/actions/post-build-source-hash/action.yml b/.github/actions/post-build-source-hash/action.yml new file mode 100644 index 00000000000..9d766767c5e --- /dev/null +++ b/.github/actions/post-build-source-hash/action.yml @@ -0,0 +1,71 @@ +name: 'Post build-source-hash commit status' +description: >- + Computes the @expo/fingerprint hash via `yarn fingerprint:generate` against + the CURRENTLY CHECKED-OUT workspace, then posts it as a `build-source-hash` + GitHub commit status on `target-sha`. This makes the fingerprint queryable + by future workflow runs so they can locate a prior run whose uploaded + native build artifacts match the current source. + +inputs: + github-token: + description: 'GitHub token with `statuses: write` permission' + required: true + target-sha: + description: >- + Commit SHA to post the `build-source-hash` status on. MUST match the + ref that was checked out before calling this action. + required: true + status-context: + description: 'GitHub commit status context name' + required: false + default: 'build-source-hash' + +outputs: + fingerprint: + description: 'The @expo/fingerprint hash that was posted' + value: ${{ steps.generate-fingerprint.outputs.fingerprint }} + +runs: + using: 'composite' + steps: + - name: Generate fingerprint + id: generate-fingerprint + shell: bash + run: | + FINGERPRINT=$(yarn fingerprint:generate) + echo "fingerprint=$FINGERPRINT" >> "$GITHUB_OUTPUT" + echo "Current fingerprint: ${FINGERPRINT}" + + - name: Post commit status + uses: actions/github-script@v7 + continue-on-error: true + env: + FINGERPRINT: ${{ steps.generate-fingerprint.outputs.fingerprint }} + STATUS_CONTEXT: ${{ inputs.status-context }} + TARGET_SHA: ${{ inputs.target-sha }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + with: + github-token: ${{ inputs.github-token }} + script: | + const { FINGERPRINT, STATUS_CONTEXT, TARGET_SHA, RUN_URL } = process.env; + if (!FINGERPRINT) { + core.setFailed('Fingerprint is empty; refusing to post status'); + return; + } + if (!TARGET_SHA) { + core.setFailed( + 'target-sha input is empty; refusing to post status ' + + '(would silently post on the wrong commit).' + ); + return; + } + await github.rest.repos.createCommitStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + sha: TARGET_SHA, + state: 'success', + context: STATUS_CONTEXT, + description: FINGERPRINT.slice(0, 140), + target_url: RUN_URL, + }); + core.info(`Posted ${STATUS_CONTEXT}=${FINGERPRINT} on ${TARGET_SHA}`); diff --git a/.github/actions/setup-e2e-env/action.yml b/.github/actions/setup-e2e-env/action.yml index e84b7cdda72..e1591398eb8 100644 --- a/.github/actions/setup-e2e-env/action.yml +++ b/.github/actions/setup-e2e-env/action.yml @@ -69,12 +69,22 @@ inputs: description: 'Whether to configure keystores for E2E tests' required: false default: 'true' + install-foundry: + description: >- + Whether to install Foundry (anvil/cast/forge) and add `node_modules/.bin` + to `$GITHUB_PATH`. Set to `'false'` when the caller already runs + `yarn setup:github-ci` afterwards (which itself runs `install:foundryup` + via `installFoundryTask`), to avoid the redundant install step. Test + runner workflows that go straight to Detox/seeder without an intervening + `yarn setup:github-ci` MUST keep this enabled (default). + required: false + default: 'true' keystore-role-to-assume: description: 'AWS IAM role to assume for keystore configuration' required: false default: 'arn:aws:iam::363762752069:role/metamask-mobile-build-signer-qa' target: - description: 'Target for which the keystore is being configured (e.g., qa, flask, main)' + description: 'Target for which the keystore is being configured (e.g. flask, main)' required: false default: 'qa' @@ -302,6 +312,7 @@ runs: YARN_ENABLE_GLOBAL_CACHE: 'true' - name: Install Foundry + if: ${{ inputs.install-foundry == 'true' }} shell: bash run: | echo "Installing Foundry via yarn install:foundryup (matches local dev and tests/seeder)..." diff --git a/.github/workflows/build-android-e2e.yml b/.github/workflows/build-android-e2e.yml index f812b2317fd..a8da0c09828 100644 --- a/.github/workflows/build-android-e2e.yml +++ b/.github/workflows/build-android-e2e.yml @@ -18,12 +18,16 @@ on: metamask_environment: description: 'The environment to build for' required: false - default: 'qa' + default: 'e2e' type: string - keystore_target: - description: 'The target to use for the keystore' + source-fingerprint: + description: >- + Canonical @expo/fingerprint hash for this commit, computed once in + the `post-build-source-hash` job. Used for cache keys and cross-run + artifact lookups. When empty (e.g. forked PR where the hash job is + skipped), the build compiles fresh with no cache reuse. required: false - default: 'qa' + default: '' type: string runner_provider: description: Runner provider forwarded from the caller @@ -50,6 +54,12 @@ jobs: - name: Checkout repo uses: actions/checkout@v6 + - name: Check force-builds override + id: force-builds + uses: ./.github/actions/check-force-builds + with: + github-token: ${{ github.token }} + - name: Configure Namespace cache if: ${{ inputs.runner_provider == 'namespace' }} uses: namespacelabs/nscloud-cache-action@15799a6b54e5765f85b2aac25b3f0df43ed571c0 # v1 @@ -62,40 +72,15 @@ jobs: ${{ env.GRADLE_USER_HOME }}/caches ${{ env.GRADLE_USER_HOME }}/wrapper - - name: Restore .metamask folder (Foundry download cache for install:foundryup) - if: ${{ inputs.runner_provider != 'namespace' }} - uses: actions/cache@v4 - with: - path: .metamask - key: .metamask-${{ hashFiles('package.json', 'yarn.lock') }} - - - name: Setup Android Build Environment - timeout-minutes: 15 - uses: ./.github/actions/setup-e2e-env - with: - platform: android - setup-simulator: false - configure-keystores: true - android-api-level: 36 - target: ${{ inputs.keystore_target }} # qa for taget=main and flask for target=flask - - - name: Setup project dependencies with retry - uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2 - with: - timeout_minutes: 10 - max_attempts: 3 - retry_wait_seconds: 30 - command: | - echo "🚀 Setting up project..." - yarn setup:github-ci --no-build-ios - - # Generate fingerprint AFTER setup but BEFORE any build modifications (the fingerprint now is fake we do not want the cached apk) - - name: Generate current fingerprint - id: generate-fingerprint + - name: Report source fingerprint run: | - FINGERPRINT=$(yarn fingerprint:generate) - echo "fingerprint=$FINGERPRINT" >> "$GITHUB_OUTPUT" - echo "Current fingerprint: ${FINGERPRINT}" + if [[ -z "$SOURCE_FINGERPRINT" ]]; then + echo "::warning::No source-fingerprint provided (likely a forked PR); artifact reuse disabled." + else + echo "Source fingerprint: $SOURCE_FINGERPRINT" + fi + env: + SOURCE_FINGERPRINT: ${{ inputs.source-fingerprint }} - name: Determine target paths and Artifact Names id: determine-target-paths @@ -117,15 +102,17 @@ jobs: exit 1 fi - # TEMPORARY: `${{ github.run_id }}` makes every key unique per workflow - # run so we always get a fresh build during the RN 0.81 upgrade — the - # `yarn fingerprint:generate` heuristic doesn't track every native input - # being changed (yarn patches, MainApplication, Podfile shims, etc.) so - # the branch cache can serve a stale .apk and only the JS gets repacked. - # Remove the trailing `-${{ github.run_id }}` from each `key:` below - # once the upgrade is settled and fingerprint covers the touched paths. + - name: Find reusable build from prior run + id: find-reusable-build + if: ${{ inputs.runner_provider != 'namespace' && steps.force-builds.outputs.force != 'true' && inputs.source-fingerprint != '' }} + uses: ./.github/actions/find-reusable-build + with: + fingerprint: ${{ inputs.source-fingerprint }} + artifact-names: '["${{ inputs.build_type }}-${{ inputs.metamask_environment }}-release.apk","${{ inputs.build_type }}-${{ inputs.metamask_environment }}-release-androidTest.apk"]' + github-token: ${{ github.token }} + - name: Restore APKs matching fingerprint from branch cache - if: ${{ inputs.runner_provider != 'namespace' }} + if: ${{ inputs.runner_provider != 'namespace' && steps.force-builds.outputs.force != 'true' && steps.find-reusable-build.outputs.found != 'true' }} id: apk-cache-restore # This action automatically updates the cache at the end of the workflow uses: cirruslabs/cache@bba69c6578b863ad0398ad40567bd2ef70290fe0 # v4 @@ -133,16 +120,10 @@ jobs: path: | ${{ steps.determine-target-paths.outputs.apk-target-path }}/${{ steps.determine-target-paths.outputs.artifact_name }}.apk ${{ steps.determine-target-paths.outputs.test-apk-target-path }}/${{ steps.determine-target-paths.outputs.artifact_name }}-androidTest.apk - # Include Gradle properties in key to force rebuild when properties change - # Keep the `hashFiles` call for Gradle config in-sync with these steps: - # - "Restore APKs matching fingerprint from branch cache" - # - "Restore APKs matching fingerprint from main cache" - # - "Restore Gradle dependencies from branch cache" - # - "Restore Gradle dependencies from main cache" - key: android-apk-${{ github.ref_name }}-${{ inputs.build_type }}-${{ env.CACHE_GENERATION }}-${{ steps.generate-fingerprint.outputs.fingerprint }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}-${{ github.run_id }} + key: android-apk-${{ github.ref_name }}-${{ inputs.build_type }}-${{ env.CACHE_GENERATION }}-${{ inputs.source-fingerprint }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - name: Restore APKs matching fingerprint from main cache - if: ${{ inputs.runner_provider != 'namespace' && steps.apk-cache-restore.outputs.cache-hit != 'true' && github.ref_name != 'main' }} + if: ${{ inputs.runner_provider != 'namespace' && steps.force-builds.outputs.force != 'true' && steps.find-reusable-build.outputs.found != 'true' && steps.apk-cache-restore.outputs.cache-hit != 'true' && github.ref_name != 'main' }} id: apk-cache-restore-main # This will only restore the cache, not update it uses: cirruslabs/cache/restore@bba69c6578b863ad0398ad40567bd2ef70290fe0 # v4 @@ -150,53 +131,142 @@ jobs: path: | ${{ steps.determine-target-paths.outputs.apk-target-path }}/${{ steps.determine-target-paths.outputs.artifact_name }}.apk ${{ steps.determine-target-paths.outputs.test-apk-target-path }}/${{ steps.determine-target-paths.outputs.artifact_name }}-androidTest.apk - # Include Gradle properties in key to force rebuild when properties change - # Keep the `hashFiles` call for Gradle config in-sync with these steps: - # - "Restore APKs matching fingerprint from branch cache" - # - "Restore APKs matching fingerprint from main cache" - # - "Restore Gradle dependencies from branch cache" - # - "Restore Gradle dependencies from main cache" - key: android-apk-main-${{ inputs.build_type }}-${{ env.CACHE_GENERATION }}-${{ steps.generate-fingerprint.outputs.fingerprint }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}-${{ github.run_id }} + key: android-apk-main-${{ inputs.build_type }}-${{ env.CACHE_GENERATION }}-${{ inputs.source-fingerprint }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + - name: Download reusable APK from prior run + id: download-reusable-apk + if: ${{ steps.find-reusable-build.outputs.found == 'true' }} + continue-on-error: true + uses: actions/download-artifact@v4 + with: + name: ${{ inputs.build_type }}-${{ inputs.metamask_environment }}-release.apk + path: ${{ steps.determine-target-paths.outputs.apk-target-path }} + github-token: ${{ github.token }} + repository: ${{ github.repository }} + run-id: ${{ steps.find-reusable-build.outputs.run-id }} + + - name: Download reusable androidTest APK from prior run + id: download-reusable-test-apk + if: ${{ steps.find-reusable-build.outputs.found == 'true' }} + continue-on-error: true + uses: actions/download-artifact@v4 + with: + name: ${{ inputs.build_type }}-${{ inputs.metamask_environment }}-release-androidTest.apk + path: ${{ steps.determine-target-paths.outputs.test-apk-target-path }} + github-token: ${{ github.token }} + repository: ${{ github.repository }} + run-id: ${{ steps.find-reusable-build.outputs.run-id }} + + - name: Log reused Android build source + if: ${{ steps.download-reusable-apk.outcome == 'success' && steps.download-reusable-test-apk.outcome == 'success' }} + run: | + echo "Reusing Android build from run ${{ steps.find-reusable-build.outputs.run-id }}" + echo "Source SHA: ${{ steps.find-reusable-build.outputs.source-sha }}" + echo "Source branch: ${{ steps.find-reusable-build.outputs.source-branch }}" + shell: bash + + - name: Compute native-build gate + id: gate + run: | + if [[ "${{ steps.find-reusable-build.outputs.found }}" == "true" \ + && "${{ steps.download-reusable-apk.outcome }}" == "success" \ + && "${{ steps.download-reusable-test-apk.outcome }}" == "success" ]]; then + echo "needs-native-build=false" >> "$GITHUB_OUTPUT" + echo "Reuse path active (cross-run artifact download succeeded); heavy Android setup + Gradle restore will be skipped." + elif [[ "${{ steps.apk-cache-restore.outputs.cache-hit }}" == "true" \ + || "${{ steps.apk-cache-restore-main.outputs.cache-hit }}" == "true" ]]; then + echo "needs-native-build=false" >> "$GITHUB_OUTPUT" + echo "APK cache hit; heavy Android setup + Gradle restore will be skipped." + else + if [[ "${{ steps.find-reusable-build.outputs.found }}" == "true" ]]; then + echo "::warning::Reusable run was found but artifact download failed (apk=${{ steps.download-reusable-apk.outcome }}, test-apk=${{ steps.download-reusable-test-apk.outcome }}); falling back to fresh native build." + fi + echo "needs-native-build=true" >> "$GITHUB_OUTPUT" + echo "No reuse path; full native build + setup will run." + fi + shell: bash + + - name: Setup Android Build Environment + timeout-minutes: 15 + uses: ./.github/actions/setup-e2e-env + with: + platform: android + setup-simulator: false + configure-keystores: true + install-foundry: false + + # The Namespace cache action above already includes `.metamask`, so the + # dedicated actions/cache restore is only needed on non-Namespace runners. + - name: Restore .metamask folder + if: ${{ inputs.runner_provider != 'namespace' }} + id: restore-metamask + uses: actions/cache@v4 + with: + path: .metamask + key: .metamask-${{ hashFiles('package.json', 'yarn.lock') }} + + - name: Setup project dependencies with retry (native-build path) + if: ${{ steps.gate.outputs.needs-native-build == 'true' }} + uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 30 + command: | + echo "🚀 Setting up project..." + yarn setup:github-ci --no-build-ios + + - name: Setup project dependencies with retry (reuse-hit path) + if: ${{ steps.gate.outputs.needs-native-build != 'true' }} + uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 30 + command: | + echo "📦 Setting up project (lightweight, skips jetify)..." + yarn setup:github-ci --no-build-ios --no-build-android + + # ------------------------------------------------------------------------- + # Gradle caches + native build — only on the full native-build path. + # ------------------------------------------------------------------------- + # Skipped on Namespace runners because the Namespace cache action above + # already handles `${GRADLE_USER_HOME}/caches` and `wrapper` — running + # cirruslabs/cache on top of it would be redundant work and a redundant + # save at end of run. - name: Restore Gradle dependencies from branch cache id: gradle-cache-restore # This action automatically updates the cache at the end of the workflow uses: cirruslabs/cache@bba69c6578b863ad0398ad40567bd2ef70290fe0 # v4 - if: ${{ inputs.runner_provider != 'namespace' && steps.apk-cache-restore.outputs.cache-hit != 'true' && steps.apk-cache-restore-main.outputs.cache-hit != 'true' }} + if: ${{ inputs.runner_provider != 'namespace' && steps.gate.outputs.needs-native-build == 'true' }} env: GRADLE_CACHE_VERSION: 1 with: path: | ~/_work/.gradle/caches ~/_work/.gradle/wrapper - # Include Gradle properties in key to force rebuild when properties change - # Keep the `hashFiles` call for Gradle config in-sync with these steps: - # - "Restore APKs matching fingerprint from branch cache" - # - "Restore APKs matching fingerprint from main cache" - # - "Restore Gradle dependencies from branch cache" - # - "Restore Gradle dependencies from main cache" - key: gradle-${{ github.ref_name }}-${{ env.GRADLE_CACHE_VERSION }}-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}-${{ github.run_id }} + # Include Gradle properties in key to force rebuild when properties change. + # Keep the `hashFiles` call for Gradle config in-sync with the + # sibling "Restore Gradle dependencies from main cache" step below. + key: gradle-${{ github.ref_name }}-${{ env.GRADLE_CACHE_VERSION }}-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - name: Restore Gradle dependencies from main cache # This will only restore the cache, not update it uses: cirruslabs/cache/restore@bba69c6578b863ad0398ad40567bd2ef70290fe0 # v4 - if: ${{ inputs.runner_provider != 'namespace' && steps.apk-cache-restore.outputs.cache-hit != 'true' && steps.apk-cache-restore-main.outputs.cache-hit != 'true' && steps.gradle-cache-restore.outputs.cache-hit != 'true' && github.ref_name != 'main' }} + if: ${{ inputs.runner_provider != 'namespace' && steps.gate.outputs.needs-native-build == 'true' && steps.gradle-cache-restore.outputs.cache-hit != 'true' && github.ref_name != 'main' }} env: GRADLE_CACHE_VERSION: 1 with: path: | ~/_work/.gradle/caches ~/_work/.gradle/wrapper - # Include Gradle properties in key to force rebuild when properties change - # Keep the `hashFiles` call for Gradle config in-sync with these steps: - # - "Restore APKs matching fingerprint from branch cache" - # - "Restore APKs matching fingerprint from main cache" - # - "Restore Gradle dependencies from branch cache" - # - "Restore Gradle dependencies from main cache" - key: gradle-main-${{ env.GRADLE_CACHE_VERSION }}-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}-${{ github.run_id }} + # Include Gradle properties in key to force rebuild when properties change. + # Keep the `hashFiles` call for Gradle config in-sync with the + # sibling "Restore Gradle dependencies from branch cache" step above. + key: gradle-main-${{ env.GRADLE_CACHE_VERSION }}-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - name: Build Android E2E APKs - if: ${{ inputs.runner_provider == 'namespace' || (steps.apk-cache-restore.outputs.cache-hit != 'true' && steps.apk-cache-restore-main.outputs.cache-hit != 'true') }} + if: ${{ steps.gate.outputs.needs-native-build == 'true' }} run: | echo "🏗 Building Android E2E APKs..." export NODE_OPTIONS="--max-old-space-size=4096" @@ -245,7 +315,7 @@ jobs: MM_PREDICT_GTM_MODAL_ENABLED: 'false' - name: Repack APK with JS updates using @expo/repack-app - if: ${{ inputs.runner_provider != 'namespace' && (steps.apk-cache-restore.outputs.cache-hit == 'true' || steps.apk-cache-restore-main.outputs.cache-hit == 'true') }} + if: ${{ steps.gate.outputs.needs-native-build != 'true' }} run: | echo "📦 Repacking APK with updated JavaScript bundle using @expo/repack-app..." # Use the optimized repack script which uses @expo/repack-app @@ -261,7 +331,7 @@ jobs: GITHUB_CI: 'true' CI: 'true' NODE_OPTIONS: '--max-old-space-size=8192' - METRO_MAX_WORKERS: '2' + METRO_MAX_WORKERS: '6' BRIDGE_USE_DEV_APIS: 'true' RAMP_INTERNAL_BUILD: 'true' SEEDLESS_ONBOARDING_ENABLED: 'true' diff --git a/.github/workflows/build-ios-e2e.yml b/.github/workflows/build-ios-e2e.yml index 2095d077355..4e99d21c48b 100644 --- a/.github/workflows/build-ios-e2e.yml +++ b/.github/workflows/build-ios-e2e.yml @@ -15,7 +15,16 @@ on: metamask_environment: description: 'The environment to build for' required: false - default: 'qa' + default: 'e2e' + type: string + source-fingerprint: + description: >- + Canonical @expo/fingerprint hash for this commit, computed once in + the `post-build-source-hash` job. Used for cache keys and cross-run + artifact lookups. When empty (e.g. forked PR where the hash job is + skipped), the build compiles fresh with no cache reuse. + required: false + default: '' type: string runner_provider: description: Runner provider forwarded from the caller @@ -23,10 +32,6 @@ on: type: string default: current -permissions: - contents: read - id-token: write - jobs: build-ios-apps: name: Build iOS E2E Apps @@ -36,9 +41,7 @@ jobs: app-uploaded: ${{ steps.upload-app.outcome == 'success' }} sourcemap-uploaded: ${{ steps.upload-sourcemap.outcome == 'success' }} env: - # Bump these to bust the respective caches and force a full rebuild - XCODE_CACHE_VERSION: 4 - IOS_APP_CACHE_VERSION: 5 + XCODE_CACHE_VERSION: 1 RCT_NO_LAUNCH_PACKAGER: 1 XCODE_BUILD_SETTINGS: 'COMPILER_INDEX_STORE_ENABLE=NO' GITHUB_CI: 'true' # This ensures it's available during pod install @@ -75,25 +78,84 @@ jobs: - name: Checkout repo uses: actions/checkout@v6 - # TEMPORARY: `${{ github.run_id }}` makes every key unique per workflow - # run so we always get a fresh build during the RN 0.81 upgrade — the - # `yarn fingerprint:generate` heuristic doesn't track every native input - # being changed (yarn patches, AppDelegate, Podfile shims, etc.) so the - # branch cache can serve a stale .app and only the JS gets repacked. - # Remove the trailing `-${{ github.run_id }}` from each `key:` below - # once the upgrade is settled and fingerprint covers the touched paths. + - name: Check force-builds override + id: force-builds + uses: ./.github/actions/check-force-builds + with: + github-token: ${{ github.token }} + + - name: Report source fingerprint + run: | + if [[ -z "$SOURCE_FINGERPRINT" ]]; then + echo "::warning::No source-fingerprint provided (likely a forked PR); artifact reuse disabled." + else + echo "Source fingerprint: $SOURCE_FINGERPRINT" + fi + env: + SOURCE_FINGERPRINT: ${{ inputs.source-fingerprint }} + + - name: Find reusable build from prior run + id: find-reusable-build + if: ${{ steps.force-builds.outputs.force != 'true' && inputs.source-fingerprint != '' }} + uses: ./.github/actions/find-reusable-build + with: + fingerprint: ${{ inputs.source-fingerprint }} + artifact-names: '["${{ inputs.build_type }}-${{ inputs.metamask_environment }}-MetaMask.app"]' + github-token: ${{ github.token }} + + - name: Download reusable iOS build from prior run + id: download-reusable-app + if: ${{ steps.find-reusable-build.outputs.found == 'true' }} + continue-on-error: true + uses: actions/download-artifact@v4 + with: + name: ${{ inputs.build_type }}-${{ inputs.metamask_environment }}-MetaMask.app + path: ios/build/Build/Products/Release-iphonesimulator/MetaMask.app + github-token: ${{ github.token }} + repository: ${{ github.repository }} + run-id: ${{ steps.find-reusable-build.outputs.run-id }} + + - name: Log reused iOS build source + if: ${{ steps.download-reusable-app.outcome == 'success' }} + run: | + echo "Reusing iOS build from run ${{ steps.find-reusable-build.outputs.run-id }}" + echo "Source SHA: ${{ steps.find-reusable-build.outputs.source-sha }}" + echo "Source branch: ${{ steps.find-reusable-build.outputs.source-branch }}" + shell: bash + + - name: Compute native-build gate + id: gate + run: | + if [[ "${{ steps.find-reusable-build.outputs.found }}" == "true" \ + && "${{ steps.download-reusable-app.outcome }}" == "success" ]]; then + echo "needs-native-build=false" >> "$GITHUB_OUTPUT" + echo "Reuse path active (cross-run artifact download succeeded); heavy native setup will be skipped." + else + if [[ "${{ steps.find-reusable-build.outputs.found }}" == "true" ]]; then + echo "::warning::Reusable run was found but artifact download failed (outcome=${{ steps.download-reusable-app.outcome }}); falling back to fresh native build." + fi + echo "needs-native-build=true" >> "$GITHUB_OUTPUT" + echo "No reuse path; full native build + setup will run." + fi + shell: bash + + # ------------------------------------------------------------------------- + # Heavy native setup — only runs on a full native-build path. + # ------------------------------------------------------------------------- + - name: Restore Xcode derived data from branch cache id: xcode-restore-cache + if: ${{ steps.gate.outputs.needs-native-build == 'true' }} # This action automatically updates the cache at the end of the workflow uses: cirruslabs/cache@bba69c6578b863ad0398ad40567bd2ef70290fe0 # v4 with: path: | ~/Library/Developer/Xcode/DerivedData ios/build - key: ${{ runner.os }}-xcode-${{ github.ref_name }}-${{ env.XCODE_CACHE_VERSION }}-${{ hashFiles('ios/**/*.{h,m,mm,swift}', 'ios/**/Podfile.lock', 'yarn.lock') }}-${{ github.run_id }} + key: ${{ runner.os }}-xcode-${{ github.ref_name }}-${{ env.XCODE_CACHE_VERSION }}-${{ hashFiles('ios/**/*.{h,m,mm,swift}', 'ios/**/Podfile.lock', 'yarn.lock') }} - name: Restore Xcode derived data from main cache - if: ${{ steps.xcode-restore-cache.outputs.cache-hit != 'true' && github.ref_name != 'main' }} + if: ${{ steps.gate.outputs.needs-native-build == 'true' && steps.xcode-restore-cache.outputs.cache-hit != 'true' && github.ref_name != 'main' }} id: xcode-restore-cache-main # This will only restore the cache, not update it uses: cirruslabs/cache/restore@bba69c6578b863ad0398ad40567bd2ef70290fe0 # v4 @@ -101,23 +163,20 @@ jobs: path: | ~/Library/Developer/Xcode/DerivedData ios/build - key: ${{ runner.os }}-xcode-main-${{ env.XCODE_CACHE_VERSION }}-${{ hashFiles('ios/**/*.{h,m,mm,swift}', 'ios/**/Podfile.lock', 'yarn.lock') }}-${{ github.run_id }} + key: ${{ runner.os }}-xcode-main-${{ env.XCODE_CACHE_VERSION }}-${{ hashFiles('ios/**/*.{h,m,mm,swift}', 'ios/**/Podfile.lock', 'yarn.lock') }} - - name: Restore .metamask folder (Foundry download cache for install:foundryup) - uses: actions/cache@v4 - with: - path: .metamask - key: .metamask-${{ hashFiles('package.json', 'yarn.lock') }} - - # Install Node.js, Xcode tools, and other iOS development dependencies + # Install Node.js, Xcode tools, and other iOS development dependencies. - name: Installing iOS Environment Setup + if: ${{ steps.gate.outputs.needs-native-build == 'true' }} timeout-minutes: 15 uses: ./.github/actions/setup-e2e-env with: platform: ios setup-simulator: false + install-foundry: false - name: Print iOS tool versions + if: ${{ steps.gate.outputs.needs-native-build == 'true' }} run: | echo "🔧 Node.js Version:" node -v || echo "Node not found" @@ -133,10 +192,20 @@ jobs: # Clean iOS plist files to prevent extended attribute issues - name: Clean iOS plist files + if: ${{ steps.gate.outputs.needs-native-build == 'true' }} run: find ios -name "*.plist" -exec xattr -c {} \; + - name: Restore .metamask folder + if: ${{ steps.gate.outputs.needs-native-build == 'true' }} + id: restore-metamask + uses: actions/cache@v4 + with: + path: .metamask + key: .metamask-${{ hashFiles('package.json', 'yarn.lock') }} + # Run project setup with retry for better resilience - name: Setup project dependencies with retry + if: ${{ steps.gate.outputs.needs-native-build == 'true' }} uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2 with: timeout_minutes: 10 @@ -146,36 +215,61 @@ jobs: echo "🚀 Setting up project..." yarn setup:github-ci --build-ios --no-build-android - # Generate fingerprint AFTER setup but BEFORE any build modifications - - name: Generate current fingerprint - id: generate-fingerprint + - name: Setup Node.js (reuse-hit path) + if: ${{ steps.gate.outputs.needs-native-build != 'true' }} + uses: actions/setup-node@v6 + with: + node-version: '20.18.0' + + - name: Enable corepack (reuse-hit path) + if: ${{ steps.gate.outputs.needs-native-build != 'true' }} run: | - FINGERPRINT=$(yarn fingerprint:generate) - echo "fingerprint=$FINGERPRINT" >> "$GITHUB_OUTPUT" - echo "Current fingerprint: ${FINGERPRINT}" + corepack enable + corepack prepare yarn@3.8.7 --activate + shell: bash - - name: Restore iOS app matching fingerprint from branch cache - id: cache-restore - # This action automatically updates the cache at the end of the workflow - uses: cirruslabs/cache@bba69c6578b863ad0398ad40567bd2ef70290fe0 # v4 + - name: Restore yarn cache (reuse-hit path) + if: ${{ steps.gate.outputs.needs-native-build != 'true' }} + uses: actions/cache@v4 with: path: | - ios/build/Build/Products/Release-iphonesimulator/MetaMask.app - key: ios-app-${{ github.ref_name }}-v${{ env.IOS_APP_CACHE_VERSION }}-${{ steps.generate-fingerprint.outputs.fingerprint }}-${{ github.run_id }} + node_modules + key: e2e-yarn-ios-${{ runner.os }}-${{ hashFiles('yarn.lock') }} - - name: Restore iOS app matching fingerprint from main cache - if: ${{ steps.cache-restore.outputs.cache-hit != 'true' && github.ref_name != 'main' }} - id: cache-restore-main - # This will only restore the cache, not update it - uses: cirruslabs/cache/restore@bba69c6578b863ad0398ad40567bd2ef70290fe0 # v4 + - name: Install JS dependencies (reuse-hit path) + if: ${{ steps.gate.outputs.needs-native-build != 'true' }} + uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2 with: - path: | - ios/build/Build/Products/Release-iphonesimulator/MetaMask.app - key: ios-app-main-v${{ env.IOS_APP_CACHE_VERSION }}-${{ steps.generate-fingerprint.outputs.fingerprint }}-${{ github.run_id }} + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 30 + command: yarn install --immutable + env: + NODE_OPTIONS: --max-old-space-size=4096 + YARN_ENABLE_GLOBAL_CACHE: 'true' + + - name: Restore .metamask folder (reuse-hit path) + if: ${{ steps.gate.outputs.needs-native-build != 'true' }} + id: restore-metamask-lean + uses: actions/cache@v4 + with: + path: .metamask + key: .metamask-${{ hashFiles('package.json', 'yarn.lock') }} + + - name: Run lightweight project setup (reuse-hit path) + if: ${{ steps.gate.outputs.needs-native-build != 'true' }} + uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 30 + command: | + echo "📦 Setting up project (lightweight, skips Xcode build)..." + yarn setup:github-ci --no-build-ios --no-build-android # Build the iOS E2E app for simulator - name: Build iOS E2E App - if: ${{ steps.cache-restore.outputs.cache-hit != 'true' && steps.cache-restore-main.outputs.cache-hit != 'true' }} + if: ${{ steps.gate.outputs.needs-native-build == 'true' }} run: | echo "🏗 Building iOS E2E App..." export NODE_OPTIONS="--max-old-space-size=8192" @@ -205,7 +299,7 @@ jobs: GOOGLE_SERVICES_B64_ANDROID: ${{ secrets.GOOGLE_SERVICES_B64_ANDROID }} - name: Repack iOS app with JS updates using @expo/repack-app - if: ${{ steps.cache-restore.outputs.cache-hit == 'true' || steps.cache-restore-main.outputs.cache-hit == 'true' }} + if: ${{ steps.gate.outputs.needs-native-build != 'true' }} run: | echo "📦 Repacking iOS app with updated JavaScript bundle using @expo/repack-app..." # Use the optimized repack script which uses @expo/repack-app @@ -221,6 +315,7 @@ jobs: GITHUB_CI: 'true' CI: 'true' NODE_OPTIONS: '--max-old-space-size=8192' + METRO_MAX_WORKERS: '6' BRIDGE_USE_DEV_APIS: 'true' RAMP_INTERNAL_BUILD: 'true' SEEDLESS_ONBOARDING_ENABLED: 'true' @@ -276,11 +371,12 @@ jobs: if-no-files-found: error continue-on-error: true - # Upload source map file for crash debugging and error tracking if exists - # Only runs when repack step runs (cache hit), as that's when sourcemap is generated + # Upload source map file for crash debugging and error tracking. + # Both paths produce it: `yarn build:ios:main:e2e` via + # `scripts/ios/bundle-js-and-sentry-upload.sh` and `yarn build:repack:ios` + # via `scripts/repack.js` both write to `sourcemaps/ios/index.js.map`. - name: Upload iOS Source Map id: upload-sourcemap - if: ${{ steps.cache-restore.outputs.cache-hit == 'true' || steps.cache-restore-main.outputs.cache-hit == 'true' }} uses: actions/upload-artifact@v4 with: name: ${{ inputs.build_type }}-${{ inputs.metamask_environment }}-index.js.map diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2f64e0a8669..33ded5310a3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -412,7 +412,7 @@ jobs: security delete-keychain "$KEYCHAIN_PATH" || true fi - # Signing: uses role + secret from builds.yml (skipped when builds.yml omits signing, e.g. simulator-only flask-dev / qa-dev) + # Signing: uses role + secret from builds.yml (skipped when builds.yml omits signing, e.g. simulator-only flask-dev) - name: Configure signing certificates if: needs.prepare.outputs.signing_aws_role != '' uses: ./.github/actions/configure-signing diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bcdf6e82a0a..e11c50aab2c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -86,6 +86,41 @@ jobs: echo "No changes detected" fi + post-build-source-hash: + name: Post build-source-hash commit status + runs-on: ubuntu-latest + if: ${{ needs.get_requirements.outputs.skip_everything != 'true' }} + permissions: + contents: read + statuses: write + needs: + - get_requirements + outputs: + fingerprint: ${{ steps.publish.outputs.fingerprint }} + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + - uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: yarn + - name: Install Yarn dependencies with retry + uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 30 + command: yarn install --immutable + - name: Post build-source-hash commit status + id: publish + uses: ./.github/actions/post-build-source-hash + with: + github-token: ${{ github.token }} + # .head.sha = PR head (pull_request events); github.sha fallback = pushed/scheduled commit + # (push to main, merge_group, schedule) where no PR payload exists. + target-sha: ${{ github.event.pull_request.head.sha || github.sha }} + dedupe: name: Dedupe runs-on: ${{ inputs.runner_provider == 'namespace' && 'namespace-profile-metamask-ci-linux' || 'ubuntu-latest' }} @@ -798,12 +833,15 @@ jobs: permissions: contents: read id-token: write - needs: [get_requirements, smart-e2e-selection] + actions: read + statuses: read + pull-requests: read + needs: [get_requirements, smart-e2e-selection, post-build-source-hash] uses: ./.github/workflows/build-android-e2e.yml with: build_type: 'main' metamask_environment: 'e2e' - keystore_target: 'qa' + source-fingerprint: ${{ needs.post-build-source-hash.outputs.fingerprint }} runner_provider: ${{ inputs.runner_provider }} secrets: inherit @@ -836,9 +874,13 @@ jobs: permissions: contents: read id-token: write - needs: [get_requirements, smart-e2e-selection] + actions: read + statuses: read + pull-requests: read + needs: [get_requirements, smart-e2e-selection, post-build-source-hash] uses: ./.github/workflows/build-ios-e2e.yml with: + source-fingerprint: ${{ needs.post-build-source-hash.outputs.fingerprint }} runner_provider: ${{ inputs.runner_provider }} secrets: inherit @@ -885,7 +927,6 @@ jobs: split_number: 1 total_splits: 1 build_type: 'main' - metamask_environment: 'qa' runner_provider: ${{ inputs.runner_provider }} secrets: inherit diff --git a/.github/workflows/run-e2e-regression-tests-android.yml b/.github/workflows/run-e2e-regression-tests-android.yml index a39a83f7a68..50354ce8f5b 100644 --- a/.github/workflows/run-e2e-regression-tests-android.yml +++ b/.github/workflows/run-e2e-regression-tests-android.yml @@ -32,7 +32,6 @@ jobs: with: build_type: 'main' metamask_environment: 'e2e' - keystore_target: 'qa' runner_provider: ${{ inputs.runner_provider }} secrets: inherit diff --git a/.github/workflows/run-e2e-smoke-tests-ios.yml b/.github/workflows/run-e2e-smoke-tests-ios.yml index 19fb3ee5d7c..37c1cbdbc11 100644 --- a/.github/workflows/run-e2e-smoke-tests-ios.yml +++ b/.github/workflows/run-e2e-smoke-tests-ios.yml @@ -39,7 +39,6 @@ jobs: total_splits: 4 changed_files: ${{ inputs.changed_files }} build_type: 'main' - metamask_environment: 'qa' runner_provider: ${{ inputs.runner_provider }} secrets: inherit @@ -58,7 +57,6 @@ jobs: total_splits: 2 changed_files: ${{ inputs.changed_files }} build_type: 'main' - metamask_environment: 'qa' runner_provider: ${{ inputs.runner_provider }} secrets: inherit @@ -77,7 +75,6 @@ jobs: total_splits: 1 changed_files: ${{ inputs.changed_files }} build_type: 'main' - metamask_environment: 'qa' runner_provider: ${{ inputs.runner_provider }} secrets: inherit @@ -96,7 +93,6 @@ jobs: total_splits: 1 changed_files: ${{ inputs.changed_files }} build_type: 'main' - metamask_environment: 'qa' runner_provider: ${{ inputs.runner_provider }} secrets: inherit @@ -115,7 +111,6 @@ jobs: total_splits: 3 changed_files: ${{ inputs.changed_files }} build_type: 'main' - metamask_environment: 'qa' runner_provider: ${{ inputs.runner_provider }} secrets: inherit @@ -134,7 +129,6 @@ jobs: total_splits: 2 changed_files: ${{ inputs.changed_files }} build_type: 'main' - metamask_environment: 'qa' runner_provider: ${{ inputs.runner_provider }} secrets: inherit @@ -153,7 +147,6 @@ jobs: total_splits: 1 changed_files: ${{ inputs.changed_files }} build_type: 'main' - metamask_environment: 'qa' runner_provider: ${{ inputs.runner_provider }} secrets: inherit @@ -172,7 +165,6 @@ jobs: total_splits: 2 changed_files: ${{ inputs.changed_files }} build_type: 'main' - metamask_environment: 'qa' runner_provider: ${{ inputs.runner_provider }} secrets: inherit @@ -191,7 +183,6 @@ jobs: total_splits: 2 changed_files: ${{ inputs.changed_files }} build_type: 'main' - metamask_environment: 'qa' runner_provider: ${{ inputs.runner_provider }} secrets: inherit @@ -210,7 +201,6 @@ jobs: total_splits: 1 changed_files: ${{ inputs.changed_files }} build_type: 'main' - metamask_environment: 'qa' runner_provider: ${{ inputs.runner_provider }} secrets: inherit @@ -229,7 +219,6 @@ jobs: total_splits: 1 changed_files: ${{ inputs.changed_files }} build_type: 'main' - metamask_environment: 'qa' runner_provider: ${{ inputs.runner_provider }} secrets: inherit @@ -248,7 +237,6 @@ jobs: total_splits: 1 changed_files: ${{ inputs.changed_files }} build_type: 'main' - metamask_environment: 'qa' runner_provider: ${{ inputs.runner_provider }} secrets: inherit @@ -267,7 +255,6 @@ jobs: total_splits: 1 changed_files: ${{ inputs.changed_files }} build_type: 'main' - metamask_environment: 'qa' runner_provider: ${{ inputs.runner_provider }} secrets: inherit @@ -286,7 +273,6 @@ jobs: total_splits: 1 changed_files: ${{ inputs.changed_files }} build_type: 'main' - metamask_environment: 'qa' runner_provider: ${{ inputs.runner_provider }} secrets: inherit @@ -305,7 +291,6 @@ jobs: total_splits: 4 changed_files: ${{ inputs.changed_files }} build_type: 'main' - metamask_environment: 'qa' runner_provider: ${{ inputs.runner_provider }} secrets: inherit diff --git a/.github/workflows/update-e2e-fixtures.yml b/.github/workflows/update-e2e-fixtures.yml index 3e25ccd123b..6f4d5fdc58e 100644 --- a/.github/workflows/update-e2e-fixtures.yml +++ b/.github/workflows/update-e2e-fixtures.yml @@ -101,6 +101,7 @@ jobs: COMMIT_SHA: ${{ steps.commit-sha.outputs.COMMIT_SHA }} BRANCH: ${{ steps.branch.outputs.BRANCH }} PR_NUMBER: ${{ needs.is-fork-pull-request.outputs.PR_NUMBER }} + RUN_ID: ${{ steps.validate-ci.outputs.RUN_ID }} steps: - uses: actions/checkout@v4 @@ -121,7 +122,8 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_NUMBER: ${{ needs.is-fork-pull-request.outputs.PR_NUMBER }} - - name: Download iOS build artifact from CI + - name: Validate CI run and resolve RUN_ID + id: validate-ci run: | COMMIT_SHA_FULL=$(git rev-parse HEAD) BRANCH="${{ steps.branch.outputs.BRANCH }}" @@ -140,6 +142,7 @@ jobs: RUN_CONCLUSION=$(echo "$RUN_INFO" | jq -r '.conclusion') RUN_HEAD_SHA=$(echo "$RUN_INFO" | jq -r '.headSha') RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${RUN_ID}" + echo "RUN_ID=${RUN_ID}" >> "$GITHUB_OUTPUT" echo "CI workflow run: ${RUN_URL}" echo "Status: ${RUN_STATUS}, Conclusion: ${RUN_CONCLUSION}" @@ -161,29 +164,14 @@ jobs: echo "::warning title=CI did not succeed::The CI workflow concluded with '${RUN_CONCLUSION}'. The iOS build artifact may not be available. View CI: ${RUN_URL}" fi - echo "::group::Downloading iOS app artifact" - echo "Attempting to download iOS app artifact from CI run ${RUN_ID}..." - - if ! gh run download "${RUN_ID}" -n main-qa-MetaMask.app -D ios-app-artifact; then - echo "::endgroup::" - echo "::error title=iOS artifact not found::Failed to download 'main-qa-MetaMask.app' artifact. Possible causes:%0A- The 'Build iOS Apps' job has not completed%0A- The iOS build was skipped (no iOS-impacting changes)%0A- The iOS build failed%0A%0ACheck the CI workflow: ${RUN_URL}" - exit 1 - fi - - echo "::endgroup::" - echo "✅ Successfully downloaded iOS app artifact." + echo "✅ CI run validated. RUN_ID=${RUN_ID}" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Cache iOS app artifact - uses: actions/cache/save@v4 - with: - path: ios-app-artifact - key: ios-app-${{ steps.commit-sha.outputs.COMMIT_SHA }} - update-fixtures: name: Export & update fixtures - needs: [prepare] + needs: [is-fork-pull-request, prepare] + if: ${{ needs.prepare.result == 'success' && needs.is-fork-pull-request.outputs.IS_FORK == 'false' }} runs-on: ${{ startsWith(github.base_ref, 'release/') && fromJSON('["ghcr.io/cirruslabs/macos-runner:tahoe"]') || fromJSON('["ghcr.io/cirruslabs/macos-runner:tahoe", "low-priority"]') }} timeout-minutes: 30 @@ -235,19 +223,22 @@ jobs: yarn detox clean-framework-cache yarn detox build-framework-cache - - name: Restore iOS app artifact - uses: actions/cache/restore@v4 - with: - path: ios-app-artifact - key: ios-app-${{ needs.prepare.outputs.COMMIT_SHA }} - fail-on-cache-miss: true - - - name: Place iOS app artifact + - name: Download iOS app artifact from CI run: | - mkdir -p artifacts/main-qa-MetaMask.app - cp -R ios-app-artifact/* artifacts/main-qa-MetaMask.app/ + RUN_ID="${{ needs.prepare.outputs.RUN_ID }}" + echo "Downloading iOS artifact from CI run ${RUN_ID}..." + + # gh run download uses the ListArtifacts API, which includes artifacts from all + # attempts of a run — including earlier attempts when only failed jobs were re-run. + if ! gh run download "${RUN_ID}" \ + -n main-qa-MetaMask.app \ + -D artifacts/main-qa-MetaMask.app; then + echo "::error title=iOS artifact not found::Failed to download 'main-qa-MetaMask.app' artifact from run ${RUN_ID}. Possible causes:%0A- The 'Build iOS Apps' job has not completed%0A- The iOS build was skipped (no iOS-impacting changes)%0A- The iOS build failed%0A%0ACheck the CI workflow: https://github.com/${{ github.repository }}/actions/runs/${RUN_ID}" + exit 1 + fi + echo "✅ Downloaded iOS artifact." env: - PREBUILT_IOS_APP_PATH: artifacts/main-qa-MetaMask.app + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Run fixture validation spec timeout-minutes: 30 @@ -259,7 +250,6 @@ jobs: PREBUILT_IOS_APP_PATH: artifacts/main-qa-MetaMask.app - name: Cache updated fixture - if: ${{ !cancelled() }} uses: actions/cache/save@v4 with: path: tests/framework/fixtures/json/default-fixture.json @@ -276,7 +266,7 @@ jobs: - prepare - is-fork-pull-request - update-fixtures - if: ${{ !cancelled() && needs.is-fork-pull-request.outputs.IS_FORK == 'false' }} + if: ${{ !cancelled() && needs.update-fixtures.result == 'success' && needs.is-fork-pull-request.outputs.IS_FORK == 'false' }} steps: - name: Checkout repository uses: actions/checkout@v4 @@ -303,17 +293,20 @@ jobs: echo "HAS_CHANGES=true" >> "$GITHUB_OUTPUT" fi - - name: Set up Node.js for prettier + - name: Set up Node.js if: steps.fixture-changes.outputs.HAS_CHANGES == 'true' uses: actions/setup-node@v4 with: node-version-file: .nvmrc + cache: yarn + + - name: Install dependencies + if: steps.fixture-changes.outputs.HAS_CHANGES == 'true' + run: yarn install --immutable - name: Format updated fixture with prettier if: steps.fixture-changes.outputs.HAS_CHANGES == 'true' - run: | - npm install --no-save --ignore-scripts prettier@^3.6.2 - ./node_modules/.bin/prettier --write tests/framework/fixtures/json/default-fixture.json + run: yarn prettier --write tests/framework/fixtures/json/default-fixture.json # Note: Commits pushed with the default GITHUB_TOKEN do not trigger # downstream CI workflows (GitHub anti-loop protection). This is diff --git a/.gitignore b/.gitignore index 8b8d9bb8a27..54545d96af0 100644 --- a/.gitignore +++ b/.gitignore @@ -110,8 +110,7 @@ licenseInfos.json # wdio wdio/reports/ -# Allows access to preview versions of @metamask/* packages for testing -.npmrc + # browserstack autogenerated files local.log diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000000..a64dff5c12b --- /dev/null +++ b/.npmrc @@ -0,0 +1,3 @@ +ignore-scripts = true +yes = false +offline = true \ No newline at end of file diff --git a/.storybook/storybook.requires.js b/.storybook/storybook.requires.js index 68b6c2694da..10dd87d50a6 100644 --- a/.storybook/storybook.requires.js +++ b/.storybook/storybook.requires.js @@ -59,7 +59,6 @@ const getStories = () => { "./app/component-library/components-temp/MultichainAccounts/MultichainAccountSelectorList/MultichainAccountSelectorList.stories.tsx": require("../app/component-library/components-temp/MultichainAccounts/MultichainAccountSelectorList/MultichainAccountSelectorList.stories.tsx"), "./app/component-library/components-temp/MultichainAccounts/MultichainAddressRow/MultichainAddressRow.stories.tsx": require("../app/component-library/components-temp/MultichainAccounts/MultichainAddressRow/MultichainAddressRow.stories.tsx"), "./app/component-library/components-temp/MultichainAccounts/MultichainAddressRowsList/MultichainAddressRowsList.stories.tsx": require("../app/component-library/components-temp/MultichainAccounts/MultichainAddressRowsList/MultichainAddressRowsList.stories.tsx"), - "./app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.stories.tsx": require("../app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.stories.tsx"), "./app/component-library/components-temp/Price/PercentageChange/PercentageChange.stories.tsx": require("../app/component-library/components-temp/Price/PercentageChange/PercentageChange.stories.tsx"), "./app/component-library/components-temp/QuickActionButtons/QuickActionButton/QuickActionButton.stories.tsx": require("../app/component-library/components-temp/QuickActionButtons/QuickActionButton/QuickActionButton.stories.tsx"), "./app/component-library/components-temp/QuickActionButtons/QuickActionButtons.stories.tsx": require("../app/component-library/components-temp/QuickActionButtons/QuickActionButtons.stories.tsx"), diff --git a/AGENTS.md b/AGENTS.md index 2f4085f823a..ee8871c8781 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -179,7 +179,6 @@ The app supports multiple build types: - `main`: Production MetaMask - `flask`: Development/experimental features -- `qa`: QA testing builds Use environment variable `METAMASK_BUILD_TYPE` to switch. diff --git a/android/app/build.gradle b/android/app/build.gradle index 3a4fe6340d6..0307e2fd43c 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -23,7 +23,7 @@ react { // The list of variants to that are debuggable. For those we're going to // skip the bundling of the JS bundle and the assets. By default is just 'debug'. // If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants. - debuggableVariants = ["qaDebug", "prodDebug", "flaskDebug"] + debuggableVariants = ["prodDebug", "flaskDebug"] /* Bundling */ // A list containing the node command and its flags. Default is just 'node'. // nodeExecutableAndArgs = ["node"] @@ -168,10 +168,10 @@ def reactNativeArchitectures() { /** -* Adding function that will retuen the Bitrise ndkPath if it is a QA or Production Build +* Adding function that will retuen the Bitrise ndkPath if it is a Production Build */ def ndkPath() { - return System.getenv('METAMASK_BUILD_TYPE') == 'qa' || System.getenv('METAMASK_ENVIRONMENT') == 'qa' || System.getenv('METAMASK_ENVIRONMENT') == 'production' ? rootProject.ext.bitriseNdkPath : "" + return System.getenv('METAMASK_ENVIRONMENT') == 'production' ? rootProject.ext.bitriseNdkPath : "" } @@ -292,12 +292,6 @@ android { keyAlias 'androiddebugkey' keyPassword 'android' } - qaProd { - storeFile file('../keystores/internalRelease.keystore') - storePassword System.getenv("BITRISEIO_ANDROID_QA_KEYSTORE_PASSWORD") - keyAlias System.getenv("BITRISEIO_ANDROID_QA_KEYSTORE_ALIAS") - keyPassword System.getenv("BITRISEIO_ANDROID_QA_KEYSTORE_PRIVATE_KEY_PASSWORD") - } } buildTypes { @@ -315,12 +309,6 @@ android { flavorDimensions "version" productFlavors { - qa { - dimension "version" - applicationIdSuffix ".qa" - applicationId "io.metamask" - signingConfig signingConfigs.qaProd - } prod { dimension "version" applicationId "io.metamask" diff --git a/android/app/google-services-example.json b/android/app/google-services-example.json index 02ea42cea0e..91ba30cea82 100644 --- a/android/app/google-services-example.json +++ b/android/app/google-services-example.json @@ -18,18 +18,6 @@ } } }, - { - "api_key": [{ "current_key": "" }], - "client_info": { - "mobilesdk_app_id": "1:123456789000:android:f1bf012572b04063", - "client_id": "android:io.metamask.qa", - "client_type": 1, - "android_client_info": { - "package_name": "io.metamask.qa", - "certificate_hash": [] - } - } - }, { "api_key": [{ "current_key": "" }], "client_info": { diff --git a/android/app/src/qa/res/drawable-hdpi/fox.png b/android/app/src/qa/res/drawable-hdpi/fox.png deleted file mode 100644 index 37732ac8555..00000000000 Binary files a/android/app/src/qa/res/drawable-hdpi/fox.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable-hdpi/node_modules_reactnavigationstack_src_views_assets_backicon.png b/android/app/src/qa/res/drawable-hdpi/node_modules_reactnavigationstack_src_views_assets_backicon.png deleted file mode 100644 index ad03a63bf3c..00000000000 Binary files a/android/app/src/qa/res/drawable-hdpi/node_modules_reactnavigationstack_src_views_assets_backicon.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable-ldpi/fox.png b/android/app/src/qa/res/drawable-ldpi/fox.png deleted file mode 100644 index a355f883857..00000000000 Binary files a/android/app/src/qa/res/drawable-ldpi/fox.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable-mdpi/app_images_astronaut.png b/android/app/src/qa/res/drawable-mdpi/app_images_astronaut.png deleted file mode 100644 index b9870ef26df..00000000000 Binary files a/android/app/src/qa/res/drawable-mdpi/app_images_astronaut.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable-mdpi/app_images_bg.png b/android/app/src/qa/res/drawable-mdpi/app_images_bg.png deleted file mode 100644 index 0ddd46b28b9..00000000000 Binary files a/android/app/src/qa/res/drawable-mdpi/app_images_bg.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable-mdpi/app_images_ethlogo.png b/android/app/src/qa/res/drawable-mdpi/app_images_ethlogo.png deleted file mode 100644 index 9fc721f6433..00000000000 Binary files a/android/app/src/qa/res/drawable-mdpi/app_images_ethlogo.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable-mdpi/app_images_fox.png b/android/app/src/qa/res/drawable-mdpi/app_images_fox.png deleted file mode 100644 index 63f9006eb93..00000000000 Binary files a/android/app/src/qa/res/drawable-mdpi/app_images_fox.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable-mdpi/app_images_foxbadge.png b/android/app/src/qa/res/drawable-mdpi/app_images_foxbadge.png deleted file mode 100644 index 923694a20df..00000000000 Binary files a/android/app/src/qa/res/drawable-mdpi/app_images_foxbadge.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable-mdpi/app_images_frame.png b/android/app/src/qa/res/drawable-mdpi/app_images_frame.png deleted file mode 100644 index b612cccfb88..00000000000 Binary files a/android/app/src/qa/res/drawable-mdpi/app_images_frame.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable-mdpi/app_images_lock.png b/android/app/src/qa/res/drawable-mdpi/app_images_lock.png deleted file mode 100644 index 2763fee33a1..00000000000 Binary files a/android/app/src/qa/res/drawable-mdpi/app_images_lock.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable-mdpi/app_images_metamaskname.png b/android/app/src/qa/res/drawable-mdpi/app_images_metamaskname.png deleted file mode 100644 index cba4a0706ff..00000000000 Binary files a/android/app/src/qa/res/drawable-mdpi/app_images_metamaskname.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable-mdpi/app_images_mminstapay.png b/android/app/src/qa/res/drawable-mdpi/app_images_mminstapay.png deleted file mode 100644 index eb2e5bebe7e..00000000000 Binary files a/android/app/src/qa/res/drawable-mdpi/app_images_mminstapay.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable-mdpi/app_images_mminstapayselected.png b/android/app/src/qa/res/drawable-mdpi/app_images_mminstapayselected.png deleted file mode 100644 index f3170fcbd76..00000000000 Binary files a/android/app/src/qa/res/drawable-mdpi/app_images_mminstapayselected.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable-mdpi/app_images_opensealogoflatcoloredblue.png b/android/app/src/qa/res/drawable-mdpi/app_images_opensealogoflatcoloredblue.png deleted file mode 100644 index 1a8a294b6b8..00000000000 Binary files a/android/app/src/qa/res/drawable-mdpi/app_images_opensealogoflatcoloredblue.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable-mdpi/app_images_paymentchannelwatermark.png b/android/app/src/qa/res/drawable-mdpi/app_images_paymentchannelwatermark.png deleted file mode 100644 index 36e8fb7a648..00000000000 Binary files a/android/app/src/qa/res/drawable-mdpi/app_images_paymentchannelwatermark.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable-mdpi/app_images_paymentchannelwelcome.png b/android/app/src/qa/res/drawable-mdpi/app_images_paymentchannelwelcome.png deleted file mode 100644 index d7095430d32..00000000000 Binary files a/android/app/src/qa/res/drawable-mdpi/app_images_paymentchannelwelcome.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable-mdpi/app_images_selectedwalleticon.png b/android/app/src/qa/res/drawable-mdpi/app_images_selectedwalleticon.png deleted file mode 100644 index 1b0e1044e80..00000000000 Binary files a/android/app/src/qa/res/drawable-mdpi/app_images_selectedwalleticon.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable-mdpi/app_images_syncicon.png b/android/app/src/qa/res/drawable-mdpi/app_images_syncicon.png deleted file mode 100644 index 68ed6f36e9d..00000000000 Binary files a/android/app/src/qa/res/drawable-mdpi/app_images_syncicon.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable-mdpi/app_images_walleticon.png b/android/app/src/qa/res/drawable-mdpi/app_images_walleticon.png deleted file mode 100644 index 7b582eb6262..00000000000 Binary files a/android/app/src/qa/res/drawable-mdpi/app_images_walleticon.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable-mdpi/fox.png b/android/app/src/qa/res/drawable-mdpi/fox.png deleted file mode 100644 index 97f613e5aed..00000000000 Binary files a/android/app/src/qa/res/drawable-mdpi/fox.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable-mdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_danger.png b/android/app/src/qa/res/drawable-mdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_danger.png deleted file mode 100644 index 4637f04abf9..00000000000 Binary files a/android/app/src/qa/res/drawable-mdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_danger.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable-mdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_info.png b/android/app/src/qa/res/drawable-mdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_info.png deleted file mode 100644 index b8caaa17d82..00000000000 Binary files a/android/app/src/qa/res/drawable-mdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_info.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable-mdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_success.png b/android/app/src/qa/res/drawable-mdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_success.png deleted file mode 100644 index 55afac478f4..00000000000 Binary files a/android/app/src/qa/res/drawable-mdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_success.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable-mdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_warning.png b/android/app/src/qa/res/drawable-mdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_warning.png deleted file mode 100644 index 1c7da1dcd90..00000000000 Binary files a/android/app/src/qa/res/drawable-mdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_warning.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable-mdpi/node_modules_reactnavigationstack_src_views_assets_backicon.png b/android/app/src/qa/res/drawable-mdpi/node_modules_reactnavigationstack_src_views_assets_backicon.png deleted file mode 100644 index 083db295f47..00000000000 Binary files a/android/app/src/qa/res/drawable-mdpi/node_modules_reactnavigationstack_src_views_assets_backicon.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable-mdpi/node_modules_reactnavigationstack_src_views_assets_backiconmask.png b/android/app/src/qa/res/drawable-mdpi/node_modules_reactnavigationstack_src_views_assets_backiconmask.png deleted file mode 100644 index dbddbdff603..00000000000 Binary files a/android/app/src/qa/res/drawable-mdpi/node_modules_reactnavigationstack_src_views_assets_backiconmask.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable-night-hdpi/fox.png b/android/app/src/qa/res/drawable-night-hdpi/fox.png deleted file mode 100644 index 37732ac8555..00000000000 Binary files a/android/app/src/qa/res/drawable-night-hdpi/fox.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable-night-ldpi/fox.png b/android/app/src/qa/res/drawable-night-ldpi/fox.png deleted file mode 100644 index a355f883857..00000000000 Binary files a/android/app/src/qa/res/drawable-night-ldpi/fox.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable-night-mdpi/fox.png b/android/app/src/qa/res/drawable-night-mdpi/fox.png deleted file mode 100644 index 97f613e5aed..00000000000 Binary files a/android/app/src/qa/res/drawable-night-mdpi/fox.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable-night-xhdpi/fox.png b/android/app/src/qa/res/drawable-night-xhdpi/fox.png deleted file mode 100644 index c1011b03d03..00000000000 Binary files a/android/app/src/qa/res/drawable-night-xhdpi/fox.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable-night-xxhdpi/fox.png b/android/app/src/qa/res/drawable-night-xxhdpi/fox.png deleted file mode 100644 index 64ea262104a..00000000000 Binary files a/android/app/src/qa/res/drawable-night-xxhdpi/fox.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable-night-xxxhdpi/fox.png b/android/app/src/qa/res/drawable-night-xxxhdpi/fox.png deleted file mode 100644 index f07158bfe99..00000000000 Binary files a/android/app/src/qa/res/drawable-night-xxxhdpi/fox.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable-night/app_background.xml b/android/app/src/qa/res/drawable-night/app_background.xml deleted file mode 100644 index 3831a860db7..00000000000 --- a/android/app/src/qa/res/drawable-night/app_background.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/android/app/src/qa/res/drawable-night/fox.png b/android/app/src/qa/res/drawable-night/fox.png deleted file mode 100644 index e8425871f2a..00000000000 Binary files a/android/app/src/qa/res/drawable-night/fox.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable-xhdpi/fox.png b/android/app/src/qa/res/drawable-xhdpi/fox.png deleted file mode 100644 index c1011b03d03..00000000000 Binary files a/android/app/src/qa/res/drawable-xhdpi/fox.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable-xhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_danger.png b/android/app/src/qa/res/drawable-xhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_danger.png deleted file mode 100644 index de61f137831..00000000000 Binary files a/android/app/src/qa/res/drawable-xhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_danger.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable-xhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_info.png b/android/app/src/qa/res/drawable-xhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_info.png deleted file mode 100644 index d97610d44ba..00000000000 Binary files a/android/app/src/qa/res/drawable-xhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_info.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable-xhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_success.png b/android/app/src/qa/res/drawable-xhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_success.png deleted file mode 100644 index 54e0d5fb969..00000000000 Binary files a/android/app/src/qa/res/drawable-xhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_success.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable-xhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_warning.png b/android/app/src/qa/res/drawable-xhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_warning.png deleted file mode 100644 index 85a3771096a..00000000000 Binary files a/android/app/src/qa/res/drawable-xhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_warning.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable-xhdpi/node_modules_reactnavigationstack_src_views_assets_backicon.png b/android/app/src/qa/res/drawable-xhdpi/node_modules_reactnavigationstack_src_views_assets_backicon.png deleted file mode 100644 index 6de0a1cbb36..00000000000 Binary files a/android/app/src/qa/res/drawable-xhdpi/node_modules_reactnavigationstack_src_views_assets_backicon.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable-xxhdpi/fox.png b/android/app/src/qa/res/drawable-xxhdpi/fox.png deleted file mode 100644 index 64ea262104a..00000000000 Binary files a/android/app/src/qa/res/drawable-xxhdpi/fox.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable-xxhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_danger.png b/android/app/src/qa/res/drawable-xxhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_danger.png deleted file mode 100644 index a1fabf69cf0..00000000000 Binary files a/android/app/src/qa/res/drawable-xxhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_danger.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable-xxhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_info.png b/android/app/src/qa/res/drawable-xxhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_info.png deleted file mode 100644 index 7686d9c3281..00000000000 Binary files a/android/app/src/qa/res/drawable-xxhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_info.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable-xxhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_success.png b/android/app/src/qa/res/drawable-xxhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_success.png deleted file mode 100644 index 6472f9d45f3..00000000000 Binary files a/android/app/src/qa/res/drawable-xxhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_success.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable-xxhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_warning.png b/android/app/src/qa/res/drawable-xxhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_warning.png deleted file mode 100644 index 8b4a90a16b6..00000000000 Binary files a/android/app/src/qa/res/drawable-xxhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_warning.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable-xxhdpi/node_modules_reactnavigationstack_src_views_assets_backicon.png b/android/app/src/qa/res/drawable-xxhdpi/node_modules_reactnavigationstack_src_views_assets_backicon.png deleted file mode 100644 index 15a983a67d9..00000000000 Binary files a/android/app/src/qa/res/drawable-xxhdpi/node_modules_reactnavigationstack_src_views_assets_backicon.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable-xxxhdpi/fox.png b/android/app/src/qa/res/drawable-xxxhdpi/fox.png deleted file mode 100644 index f07158bfe99..00000000000 Binary files a/android/app/src/qa/res/drawable-xxxhdpi/fox.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable-xxxhdpi/node_modules_reactnavigationstack_src_views_assets_backicon.png b/android/app/src/qa/res/drawable-xxxhdpi/node_modules_reactnavigationstack_src_views_assets_backicon.png deleted file mode 100644 index 17e52e8550e..00000000000 Binary files a/android/app/src/qa/res/drawable-xxxhdpi/node_modules_reactnavigationstack_src_views_assets_backicon.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable/app_background.xml b/android/app/src/qa/res/drawable/app_background.xml deleted file mode 100644 index 3831a860db7..00000000000 --- a/android/app/src/qa/res/drawable/app_background.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/android/app/src/qa/res/drawable/fox.png b/android/app/src/qa/res/drawable/fox.png deleted file mode 100644 index e8425871f2a..00000000000 Binary files a/android/app/src/qa/res/drawable/fox.png and /dev/null differ diff --git a/android/app/src/qa/res/drawable/rn_edit_text_material.xml b/android/app/src/qa/res/drawable/rn_edit_text_material.xml deleted file mode 100644 index a902b2a3a60..00000000000 --- a/android/app/src/qa/res/drawable/rn_edit_text_material.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - diff --git a/android/app/src/qa/res/ic_launcher_background.png b/android/app/src/qa/res/ic_launcher_background.png deleted file mode 100644 index 82541d29024..00000000000 Binary files a/android/app/src/qa/res/ic_launcher_background.png and /dev/null differ diff --git a/android/app/src/qa/res/ic_launcher_foreground.png b/android/app/src/qa/res/ic_launcher_foreground.png deleted file mode 100644 index edd003df7a8..00000000000 Binary files a/android/app/src/qa/res/ic_launcher_foreground.png and /dev/null differ diff --git a/android/app/src/qa/res/layout-night/launch_screen.xml b/android/app/src/qa/res/layout-night/launch_screen.xml deleted file mode 100644 index 04b0165a649..00000000000 --- a/android/app/src/qa/res/layout-night/launch_screen.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - diff --git a/android/app/src/qa/res/layout/launch_screen.xml b/android/app/src/qa/res/layout/launch_screen.xml deleted file mode 100644 index 04b0165a649..00000000000 --- a/android/app/src/qa/res/layout/launch_screen.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - diff --git a/android/app/src/qa/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/qa/res/mipmap-anydpi-v26/ic_launcher.xml deleted file mode 100644 index 4ae7d12378f..00000000000 --- a/android/app/src/qa/res/mipmap-anydpi-v26/ic_launcher.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/android/app/src/qa/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/app/src/qa/res/mipmap-anydpi-v26/ic_launcher_round.xml deleted file mode 100644 index 4ae7d12378f..00000000000 --- a/android/app/src/qa/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/android/app/src/qa/res/mipmap-hdpi/ic_launcher.png b/android/app/src/qa/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index b80692c622b..00000000000 Binary files a/android/app/src/qa/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/android/app/src/qa/res/mipmap-hdpi/ic_launcher_background.png b/android/app/src/qa/res/mipmap-hdpi/ic_launcher_background.png deleted file mode 100644 index 2f46ed45798..00000000000 Binary files a/android/app/src/qa/res/mipmap-hdpi/ic_launcher_background.png and /dev/null differ diff --git a/android/app/src/qa/res/mipmap-hdpi/ic_launcher_foreground.png b/android/app/src/qa/res/mipmap-hdpi/ic_launcher_foreground.png deleted file mode 100644 index 6774d1202a4..00000000000 Binary files a/android/app/src/qa/res/mipmap-hdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/android/app/src/qa/res/mipmap-hdpi/ic_launcher_round.png b/android/app/src/qa/res/mipmap-hdpi/ic_launcher_round.png deleted file mode 100644 index 56096d85605..00000000000 Binary files a/android/app/src/qa/res/mipmap-hdpi/ic_launcher_round.png and /dev/null differ diff --git a/android/app/src/qa/res/mipmap-hdpi/ic_notification.png b/android/app/src/qa/res/mipmap-hdpi/ic_notification.png deleted file mode 100644 index c67eb1e10ac..00000000000 Binary files a/android/app/src/qa/res/mipmap-hdpi/ic_notification.png and /dev/null differ diff --git a/android/app/src/qa/res/mipmap-hdpi/ic_notification_small.png b/android/app/src/qa/res/mipmap-hdpi/ic_notification_small.png deleted file mode 100644 index 3b8f68ff16b..00000000000 Binary files a/android/app/src/qa/res/mipmap-hdpi/ic_notification_small.png and /dev/null differ diff --git a/android/app/src/qa/res/mipmap-hdpi/splash.png b/android/app/src/qa/res/mipmap-hdpi/splash.png deleted file mode 100644 index 85315c64d94..00000000000 Binary files a/android/app/src/qa/res/mipmap-hdpi/splash.png and /dev/null differ diff --git a/android/app/src/qa/res/mipmap-mdpi/ic_launcher.png b/android/app/src/qa/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 6f43786198d..00000000000 Binary files a/android/app/src/qa/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/android/app/src/qa/res/mipmap-mdpi/ic_launcher_background.png b/android/app/src/qa/res/mipmap-mdpi/ic_launcher_background.png deleted file mode 100644 index 13f94bdb9cd..00000000000 Binary files a/android/app/src/qa/res/mipmap-mdpi/ic_launcher_background.png and /dev/null differ diff --git a/android/app/src/qa/res/mipmap-mdpi/ic_launcher_foreground.png b/android/app/src/qa/res/mipmap-mdpi/ic_launcher_foreground.png deleted file mode 100644 index a0726b11ff1..00000000000 Binary files a/android/app/src/qa/res/mipmap-mdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/android/app/src/qa/res/mipmap-mdpi/ic_launcher_round.png b/android/app/src/qa/res/mipmap-mdpi/ic_launcher_round.png deleted file mode 100644 index 56fee06d605..00000000000 Binary files a/android/app/src/qa/res/mipmap-mdpi/ic_launcher_round.png and /dev/null differ diff --git a/android/app/src/qa/res/mipmap-mdpi/ic_notification.png b/android/app/src/qa/res/mipmap-mdpi/ic_notification.png deleted file mode 100644 index ee3a4812137..00000000000 Binary files a/android/app/src/qa/res/mipmap-mdpi/ic_notification.png and /dev/null differ diff --git a/android/app/src/qa/res/mipmap-mdpi/ic_notification_small.png b/android/app/src/qa/res/mipmap-mdpi/ic_notification_small.png deleted file mode 100644 index 2fb6dc8f9ae..00000000000 Binary files a/android/app/src/qa/res/mipmap-mdpi/ic_notification_small.png and /dev/null differ diff --git a/android/app/src/qa/res/mipmap-mdpi/splash.png b/android/app/src/qa/res/mipmap-mdpi/splash.png deleted file mode 100644 index 85315c64d94..00000000000 Binary files a/android/app/src/qa/res/mipmap-mdpi/splash.png and /dev/null differ diff --git a/android/app/src/qa/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/qa/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index 2baa35ac7e7..00000000000 Binary files a/android/app/src/qa/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/android/app/src/qa/res/mipmap-xhdpi/ic_launcher_background.png b/android/app/src/qa/res/mipmap-xhdpi/ic_launcher_background.png deleted file mode 100644 index d446ff5d52b..00000000000 Binary files a/android/app/src/qa/res/mipmap-xhdpi/ic_launcher_background.png and /dev/null differ diff --git a/android/app/src/qa/res/mipmap-xhdpi/ic_launcher_foreground.png b/android/app/src/qa/res/mipmap-xhdpi/ic_launcher_foreground.png deleted file mode 100644 index 049524c5fd9..00000000000 Binary files a/android/app/src/qa/res/mipmap-xhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/android/app/src/qa/res/mipmap-xhdpi/ic_launcher_round.png b/android/app/src/qa/res/mipmap-xhdpi/ic_launcher_round.png deleted file mode 100644 index 7e328b08385..00000000000 Binary files a/android/app/src/qa/res/mipmap-xhdpi/ic_launcher_round.png and /dev/null differ diff --git a/android/app/src/qa/res/mipmap-xhdpi/ic_notification.png b/android/app/src/qa/res/mipmap-xhdpi/ic_notification.png deleted file mode 100644 index b4d00f06442..00000000000 Binary files a/android/app/src/qa/res/mipmap-xhdpi/ic_notification.png and /dev/null differ diff --git a/android/app/src/qa/res/mipmap-xhdpi/ic_notification_small.png b/android/app/src/qa/res/mipmap-xhdpi/ic_notification_small.png deleted file mode 100644 index bcc5588cf5e..00000000000 Binary files a/android/app/src/qa/res/mipmap-xhdpi/ic_notification_small.png and /dev/null differ diff --git a/android/app/src/qa/res/mipmap-xhdpi/splash.png b/android/app/src/qa/res/mipmap-xhdpi/splash.png deleted file mode 100644 index 0682a1393ef..00000000000 Binary files a/android/app/src/qa/res/mipmap-xhdpi/splash.png and /dev/null differ diff --git a/android/app/src/qa/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/qa/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index d9bfc2322b5..00000000000 Binary files a/android/app/src/qa/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/android/app/src/qa/res/mipmap-xxhdpi/ic_launcher_background.png b/android/app/src/qa/res/mipmap-xxhdpi/ic_launcher_background.png deleted file mode 100644 index 8f84cfc799e..00000000000 Binary files a/android/app/src/qa/res/mipmap-xxhdpi/ic_launcher_background.png and /dev/null differ diff --git a/android/app/src/qa/res/mipmap-xxhdpi/ic_launcher_foreground.png b/android/app/src/qa/res/mipmap-xxhdpi/ic_launcher_foreground.png deleted file mode 100644 index ba4bcc4740f..00000000000 Binary files a/android/app/src/qa/res/mipmap-xxhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/android/app/src/qa/res/mipmap-xxhdpi/ic_launcher_round.png b/android/app/src/qa/res/mipmap-xxhdpi/ic_launcher_round.png deleted file mode 100644 index 498c160d402..00000000000 Binary files a/android/app/src/qa/res/mipmap-xxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/android/app/src/qa/res/mipmap-xxhdpi/ic_notification.png b/android/app/src/qa/res/mipmap-xxhdpi/ic_notification.png deleted file mode 100644 index d3a87f925bf..00000000000 Binary files a/android/app/src/qa/res/mipmap-xxhdpi/ic_notification.png and /dev/null differ diff --git a/android/app/src/qa/res/mipmap-xxhdpi/ic_notification_small.png b/android/app/src/qa/res/mipmap-xxhdpi/ic_notification_small.png deleted file mode 100644 index 41645857128..00000000000 Binary files a/android/app/src/qa/res/mipmap-xxhdpi/ic_notification_small.png and /dev/null differ diff --git a/android/app/src/qa/res/mipmap-xxhdpi/splash.png b/android/app/src/qa/res/mipmap-xxhdpi/splash.png deleted file mode 100644 index 0228e2cea26..00000000000 Binary files a/android/app/src/qa/res/mipmap-xxhdpi/splash.png and /dev/null differ diff --git a/android/app/src/qa/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/qa/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 5e31dd38e4c..00000000000 Binary files a/android/app/src/qa/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/android/app/src/qa/res/mipmap-xxxhdpi/ic_launcher_background.png b/android/app/src/qa/res/mipmap-xxxhdpi/ic_launcher_background.png deleted file mode 100644 index 925ece2ad5b..00000000000 Binary files a/android/app/src/qa/res/mipmap-xxxhdpi/ic_launcher_background.png and /dev/null differ diff --git a/android/app/src/qa/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/android/app/src/qa/res/mipmap-xxxhdpi/ic_launcher_foreground.png deleted file mode 100644 index 75d4c0b32f1..00000000000 Binary files a/android/app/src/qa/res/mipmap-xxxhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/android/app/src/qa/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/app/src/qa/res/mipmap-xxxhdpi/ic_launcher_round.png deleted file mode 100644 index 0f86a95918b..00000000000 Binary files a/android/app/src/qa/res/mipmap-xxxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/android/app/src/qa/res/mipmap-xxxhdpi/ic_notification.png b/android/app/src/qa/res/mipmap-xxxhdpi/ic_notification.png deleted file mode 100644 index 29a3a3f9ed5..00000000000 Binary files a/android/app/src/qa/res/mipmap-xxxhdpi/ic_notification.png and /dev/null differ diff --git a/android/app/src/qa/res/mipmap-xxxhdpi/ic_notification_small.png b/android/app/src/qa/res/mipmap-xxxhdpi/ic_notification_small.png deleted file mode 100644 index b416b0c6cb2..00000000000 Binary files a/android/app/src/qa/res/mipmap-xxxhdpi/ic_notification_small.png and /dev/null differ diff --git a/android/app/src/qa/res/mipmap-xxxhdpi/splash.png b/android/app/src/qa/res/mipmap-xxxhdpi/splash.png deleted file mode 100644 index fdfbb0ccee1..00000000000 Binary files a/android/app/src/qa/res/mipmap-xxxhdpi/splash.png and /dev/null differ diff --git a/android/app/src/qa/res/values-night/booleans.xml b/android/app/src/qa/res/values-night/booleans.xml deleted file mode 100644 index 0c4442defd9..00000000000 --- a/android/app/src/qa/res/values-night/booleans.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - false - diff --git a/android/app/src/qa/res/values-night/colors.xml b/android/app/src/qa/res/values-night/colors.xml deleted file mode 100644 index cc8e5b12813..00000000000 --- a/android/app/src/qa/res/values-night/colors.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - #151719 - #FFFFFF - #000000 - #EBEBED - #000000 - #FFFFFF - #2A4174 - diff --git a/android/app/src/qa/res/values-night/strings.xml b/android/app/src/qa/res/values-night/strings.xml deleted file mode 100644 index 7f5cdee58df..00000000000 --- a/android/app/src/qa/res/values-night/strings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - MetaMask - diff --git a/android/app/src/qa/res/values-night/styles.xml b/android/app/src/qa/res/values-night/styles.xml deleted file mode 100644 index f26b6729341..00000000000 --- a/android/app/src/qa/res/values-night/styles.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - diff --git a/android/app/src/qa/res/values/booleans.xml b/android/app/src/qa/res/values/booleans.xml deleted file mode 100644 index 7b6c18b8c44..00000000000 --- a/android/app/src/qa/res/values/booleans.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - true - diff --git a/android/app/src/qa/res/values/colors.xml b/android/app/src/qa/res/values/colors.xml deleted file mode 100644 index 187da3782d1..00000000000 --- a/android/app/src/qa/res/values/colors.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - #FFFFFF - #000000 - #000000 - #EBEBED - #000000 - #FFFFFF - #2A4174 - diff --git a/android/app/src/qa/res/values/strings.xml b/android/app/src/qa/res/values/strings.xml deleted file mode 100644 index 9ecf7807eb0..00000000000 --- a/android/app/src/qa/res/values/strings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - MetaMask QA - diff --git a/android/app/src/qa/res/values/styles.xml b/android/app/src/qa/res/values/styles.xml deleted file mode 100644 index a9d908ef72f..00000000000 --- a/android/app/src/qa/res/values/styles.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - diff --git a/android/app/src/qa/res/xml/filepaths.xml b/android/app/src/qa/res/xml/filepaths.xml deleted file mode 100644 index 201556853e7..00000000000 --- a/android/app/src/qa/res/xml/filepaths.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/android/app/src/qa/res/xml/react_native_config.xml b/android/app/src/qa/res/xml/react_native_config.xml deleted file mode 100644 index 313da0eedad..00000000000 --- a/android/app/src/qa/res/xml/react_native_config.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - sslip.io - localhost - 10.0.2.2 - 10.0.3.2 - - - diff --git a/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.stories.tsx b/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.stories.tsx deleted file mode 100644 index 666439a3648..00000000000 --- a/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.stories.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import React from 'react'; -import { Provider } from 'react-redux'; -import AggregatedPercentage from './AggregatedPercentage'; -import { createStore } from 'redux'; -import initialBackgroundState from '../../../../util/test/initial-background-state.json'; -import { AggregatedPercentageProps } from './AggregatedPercentage.types'; -const mockInitialState = { - engine: { - backgroundState: initialBackgroundState, - }, -}; - -const rootReducer = (state = mockInitialState) => state; -const store = createStore(rootReducer); - -export default { - title: 'Component Library / AggregatedPercentage', - component: AggregatedPercentage, - decorators: [ - (Story: typeof React.Component) => ( - - - - ), - ], -}; - -const Template = (args: AggregatedPercentageProps) => ( - -); - -export const Default = Template.bind( - {}, - { - ethFiat: 1000, - tokenFiat: 500, - tokenFiat1dAgo: 950, - ethFiat1dAgo: 450, - }, -); - -export const NegativePercentageChange: ( - args: AggregatedPercentageProps, -) => void = Template.bind( - {}, - { - ethFiat: 900, - tokenFiat: 400, - tokenFiat1dAgo: 950, - ethFiat1dAgo: 1000, - }, -); - -export const PositivePercentageChange = Template.bind( - {}, - { - ethFiat: 1100, - tokenFiat: 600, - tokenFiat1dAgo: 500, - ethFiat1dAgo: 1000, - }, -); - -export const MixedPercentageChange = Template.bind( - {}, - { - ethFiat: 1050, - tokenFiat: 450, - tokenFiat1dAgo: 500, - ethFiat1dAgo: 1000, - }, -); diff --git a/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.test.tsx b/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.test.tsx deleted file mode 100644 index d7d9d5ee32f..00000000000 --- a/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.test.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import React from 'react'; -import { render } from '@testing-library/react-native'; -import AggregatedPercentage from './AggregatedPercentage'; -import { mockTheme } from '../../../../util/theme'; -import { useSelector } from 'react-redux'; -import { selectCurrentCurrency } from '../../../../selectors/currencyRateController'; -import { - FORMATTED_VALUE_PRICE_TEST_ID, - FORMATTED_PERCENTAGE_TEST_ID, -} from './AggregatedPercentage.constants'; - -jest.mock('react-redux', () => ({ - ...jest.requireActual('react-redux'), - useSelector: jest.fn(), -})); -describe('AggregatedPercentage', () => { - beforeEach(() => { - (useSelector as jest.Mock).mockImplementation((selector) => { - if (selector === selectCurrentCurrency) return 'USD'; - }); - }); - afterEach(() => { - (useSelector as jest.Mock).mockClear(); - }); - it('renders percentage and value price elements', () => { - const { getByTestId } = render( - , - ); - expect(getByTestId(FORMATTED_PERCENTAGE_TEST_ID)).toBeOnTheScreen(); - expect(getByTestId(FORMATTED_VALUE_PRICE_TEST_ID)).toBeOnTheScreen(); - }); - - it('renders positive percentage change correctly', () => { - const { getByText } = render( - , - ); - - expect(getByText('(+25.00%)')).toBeOnTheScreen(); - expect(getByText('+100 USD')).toBeOnTheScreen(); - - expect(getByText('(+25.00%)').props.style).toMatchObject({ - color: mockTheme.colors.success.default, - }); - }); - - it('renders negative percentage change correctly', () => { - const { getByText } = render( - , - ); - - expect(getByText('(-30.00%)')).toBeOnTheScreen(); - expect(getByText('-150 USD')).toBeOnTheScreen(); - - expect(getByText('(-30.00%)').props.style).toMatchObject({ - color: mockTheme.colors.error.default, - }); - }); - - it('renders correctly with privacy mode on', () => { - const { getByTestId } = render( - , - ); - - const formattedPercentage = getByTestId(FORMATTED_PERCENTAGE_TEST_ID); - const formattedValuePrice = getByTestId(FORMATTED_VALUE_PRICE_TEST_ID); - - expect(formattedPercentage.props.children).toBe('••••••••••'); - expect(formattedValuePrice.props.children).toBe('••••••••••'); - }); -}); diff --git a/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.tsx b/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.tsx deleted file mode 100644 index a56675d122b..00000000000 --- a/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import React from 'react'; -import { - TextColor, - TextVariant, -} from '../../../../component-library/components/Texts/Text'; -import SensitiveText from '../../../../component-library/components/Texts/SensitiveText'; -import { View } from 'react-native'; -import { renderFiat } from '../../../../util/number'; -import { useSelector } from 'react-redux'; -import { selectCurrentCurrency } from '../../../../selectors/currencyRateController'; -import styleSheet from './AggregatedPercentage.styles'; -import { useStyles } from '../../../hooks'; -import { - FORMATTED_VALUE_PRICE_TEST_ID, - FORMATTED_PERCENTAGE_TEST_ID, -} from './AggregatedPercentage.constants'; -import { DECIMALS_TO_SHOW } from '../../../../components/UI/Tokens/constants'; -import { AggregatedPercentageProps } from './AggregatedPercentage.types'; - -const isValidAmount = (amount: number | null | undefined): boolean => - amount !== null && amount !== undefined && !Number.isNaN(amount); - -const AggregatedPercentage = ({ - ethFiat, - tokenFiat, - tokenFiat1dAgo, - ethFiat1dAgo, - privacyMode = false, -}: AggregatedPercentageProps) => { - const { styles } = useStyles(styleSheet, {}); - - const currentCurrency = useSelector(selectCurrentCurrency); - - const totalBalance = ethFiat + tokenFiat; - const totalBalance1dAgo = ethFiat1dAgo + tokenFiat1dAgo; - - const amountChange = totalBalance - totalBalance1dAgo; - - const percentageChange = - ((totalBalance - totalBalance1dAgo) / totalBalance1dAgo) * 100 || 0; - - let percentageTextColor = TextColor.Default; - - if (!privacyMode) { - if (percentageChange === 0) { - percentageTextColor = TextColor.Default; - } else if (percentageChange > 0) { - percentageTextColor = TextColor.Success; - } else { - percentageTextColor = TextColor.Error; - } - } else { - percentageTextColor = TextColor.Alternative; - } - - const formattedPercentage = isValidAmount(percentageChange) - ? `(${(percentageChange as number) >= 0 ? '+' : ''}${( - percentageChange as number - ).toFixed(2)}%)` - : ''; - - const formattedValuePrice = isValidAmount(amountChange) - ? `${(amountChange as number) >= 0 ? '+' : ''}${renderFiat( - amountChange, - currentCurrency, - DECIMALS_TO_SHOW, - )} ` - : ''; - - return ( - - - {formattedValuePrice} - - - {formattedPercentage} - - - ); -}; - -export default AggregatedPercentage; diff --git a/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.types.ts b/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.types.ts deleted file mode 100644 index 45fd7d283dc..00000000000 --- a/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.types.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface AggregatedPercentageProps { - ethFiat: number; - tokenFiat: number; - tokenFiat1dAgo: number; - ethFiat1dAgo: number; - privacyMode?: boolean; -} diff --git a/app/component-library/components-temp/Price/AggregatedPercentage/index.ts b/app/component-library/components-temp/Price/AggregatedPercentage/index.ts deleted file mode 100644 index 3e7965d02fa..00000000000 --- a/app/component-library/components-temp/Price/AggregatedPercentage/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './AggregatedPercentage'; diff --git a/app/component-library/components-temp/Price/AggregatedPercentage/utils.ts b/app/component-library/components-temp/Price/AggregatedPercentage/utils.ts index 5c0602db9e3..bd58526ef45 100644 --- a/app/component-library/components-temp/Price/AggregatedPercentage/utils.ts +++ b/app/component-library/components-temp/Price/AggregatedPercentage/utils.ts @@ -1,16 +1,18 @@ import i18n from '../../../../../locales/i18n'; import { DECIMALS_TO_SHOW } from '../../../../components/UI/Tokens/constants'; import { formatWithThreshold } from '../../../../util/assets'; -import { renderFiat } from '../../../../util/number'; +import { renderFiat } from '../../../../util/number/bigint'; import { TextColor } from '../../../components/Texts/Text'; +type RenderFiatCurrencyCode = Parameters[1]; + export const getFormattedAmountChange = ( input: number, currentCurrency: string, ) => `${input >= 0 ? '+' : ''}${renderFiat( input, - currentCurrency, + currentCurrency as RenderFiatCurrencyCode, DECIMALS_TO_SHOW, )} `; diff --git a/app/components/Base/ScreenView.tsx b/app/components/Base/ScreenView.tsx index e4b425711c1..e4db11ff2e8 100644 --- a/app/components/Base/ScreenView.tsx +++ b/app/components/Base/ScreenView.tsx @@ -1,7 +1,7 @@ import type { ThemeColors } from '@metamask/design-tokens'; import React from 'react'; -import { StyleSheet, ScrollView } from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; +import { StyleSheet, ScrollView, View, ScrollViewProps } from 'react-native'; +import { SafeAreaView, type Edge } from 'react-native-safe-area-context'; import { useTheme } from '../../util/theme'; const createStyles = (colors: ThemeColors) => @@ -12,16 +12,34 @@ const createStyles = (colors: ThemeColors) => }, }); -interface ScreenViewProps { +interface ScreenViewProps extends ScrollViewProps { children: React.ReactNode; + /** + * Safe-area edges applied by the internal SafeAreaView wrapper. + * Defaults to `['bottom', 'left', 'right']` for backwards compatibility. + * Pass `[]` when an ancestor SafeAreaView already handles the insets to + * avoid double padding. + */ + safeAreaEdges?: readonly Edge[]; } -const ScreenView: React.FC = (props) => { +const ScreenView: React.FC = ({ + safeAreaEdges = ['bottom', 'left', 'right'], + ...props +}) => { const { colors } = useTheme(); const styles = createStyles(colors); + if (safeAreaEdges.length === 0) { + return ( + + + + ); + } + return ( - + ); diff --git a/app/components/UI/Bridge/Views/BridgeView/BridgeView.styles.ts b/app/components/UI/Bridge/Views/BridgeView/BridgeView.styles.ts index 22c4221f321..6a7015861f3 100644 --- a/app/components/UI/Bridge/Views/BridgeView/BridgeView.styles.ts +++ b/app/components/UI/Bridge/Views/BridgeView/BridgeView.styles.ts @@ -10,6 +10,10 @@ export const createStyles = (params: { theme: Theme }) => { screen: { flex: 1, }, + screenWrapper: { + flex: 1, + backgroundColor: theme.colors.background.default, + }, inputsContainer: { paddingVertical: 12, paddingHorizontal: 16, diff --git a/app/components/UI/Bridge/Views/BridgeView/index.tsx b/app/components/UI/Bridge/Views/BridgeView/index.tsx index 0483c61094e..b540f7389e1 100644 --- a/app/components/UI/Bridge/Views/BridgeView/index.tsx +++ b/app/components/UI/Bridge/Views/BridgeView/index.tsx @@ -18,6 +18,7 @@ import { BannerAlert, BannerAlertSeverity, Box, + HeaderStandard, Icon, IconColor, IconName, @@ -56,9 +57,9 @@ import { useFocusEffect, type RouteProp, } from '@react-navigation/native'; -import { getBridgeNavbar } from '../../../Navbar'; import { useTheme } from '../../../../../util/theme'; import { strings } from '../../../../../../locales/i18n'; +import { BridgeViewMode } from '../../types'; import Engine from '../../../../../core/Engine'; import Routes from '../../../../../constants/navigation/Routes'; import QuoteDetailsCard from '../../components/QuoteDetailsCard'; @@ -81,6 +82,7 @@ import { type NativeSyntheticEvent, type NativeScrollEvent, } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; import useIsInsufficientBalance from '../../hooks/useInsufficientBalance'; import { selectSelectedInternalAccountFormattedAddress } from '../../../../../selectors/accountsController'; import { isHardwareAccount } from '../../../../../util/address'; @@ -351,9 +353,17 @@ const BridgeViewContent = ({ latestSourceBalance }: BridgeViewContentProps) => { [dispatch], ); - useEffect(() => { - navigation.setOptions(getBridgeNavbar(navigation, bridgeViewMode, colors)); - }, [navigation, bridgeViewMode, colors]); + let headerTitle: string; + if (bridgeViewMode === BridgeViewMode.Bridge) { + headerTitle = strings('bridge.title'); + } else if ( + bridgeViewMode === BridgeViewMode.Swap || + bridgeViewMode === BridgeViewMode.Unified + ) { + headerTitle = strings('swaps.title'); + } else { + headerTitle = `${strings('swaps.title')}/${strings('bridge.title')}`; + } useTrackSwapPageViewed(); @@ -426,280 +436,291 @@ const BridgeViewContent = ({ latestSourceBalance }: BridgeViewContentProps) => { ); return ( - // Need this to be full height of screen - // @ts-expect-error The type is incorrect, this will work - - !shouldShowTrendingTokens} - onResponderRelease={dismissInputAndKeypad} - > - + navigation.goBack()} + includesTopInset + /> + + !shouldShowTrendingTokens} + onResponderRelease={dismissInputAndKeypad} > - - keypadRef.current?.open()} - onSelectionChange={handleSourceSelectionChange} - onTokenPress={handleSourceTokenPress} - onMaxPress={handleSourceMaxPress} - latestAtomicBalance={latestSourceBalance?.atomicBalance} - isSourceToken - isQuoteSponsored={isQuoteSponsored} - /> - - keypadRef.current?.close()} - onTokenPress={handleDestTokenPress} - isLoading={!destTokenAmount && isLoading} - style={styles.destTokenArea} - isQuoteSponsored={isQuoteSponsored} - /> - - - - {quoteStreamComplete?.reason || quoteFetchError - ? (() => { - const quoteStreamErrorBannerStyle = { - borderLeftWidth: 4, - borderColor: colors.error.default, - backgroundColor: colors.error.muted, - paddingLeft: 8, - }; - return ( - - } - description={getQuoteStreamReasonString( - quoteStreamComplete?.reason, - )} - /> - ); - })() - : null} - - {tokenWarning - ? (() => { - const isMalicious = - tokenWarning.type === SecurityDataType.Malicious; - const bannerColors = isMalicious - ? colors.error - : colors.warning; - const bannerStyle = { - borderLeftWidth: 4, - borderColor: bannerColors.default, - backgroundColor: bannerColors.muted, - paddingLeft: 8, - }; - const securityConfig = getBridgeTokenSecurityConfig( - tokenWarning.type, - ); - const navigateToModal = () => - navigation.navigate(Routes.BRIDGE.MODALS.ROOT, { - screen: Routes.BRIDGE.MODALS.TOKEN_WARNING_MODAL, - params: { - warningType: tokenWarning.type, - features: tokenWarning.metadata?.features ?? [], - mode: TokenWarningModalMode.Info, - location, - }, - }); - return ( - + + + keypadRef.current?.open()} + onSelectionChange={handleSourceSelectionChange} + onTokenPress={handleSourceTokenPress} + onMaxPress={handleSourceMaxPress} + latestAtomicBalance={latestSourceBalance?.atomicBalance} + isSourceToken + isQuoteSponsored={isQuoteSponsored} + /> + + keypadRef.current?.close()} + onTokenPress={handleDestTokenPress} + isLoading={!destTokenAmount && isLoading} + style={styles.destTokenArea} + isQuoteSponsored={isQuoteSponsored} + /> + + + + {quoteStreamComplete?.reason || quoteFetchError + ? (() => { + const quoteStreamErrorBannerStyle = { + borderLeftWidth: 4, + borderColor: colors.error.default, + backgroundColor: colors.error.muted, + paddingLeft: 8, + }; + return ( } - description={ - isMalicious - ? strings('bridge.token_warning_malicious_banner', { - token: destToken?.symbol, - }) - : strings( - 'bridge.token_warning_suspicious_banner', - { - token: destToken?.symbol, - }, - ) - } - onClose={navigateToModal} - closeButtonProps={{ iconName: CLIconName.ArrowRight }} + description={getQuoteStreamReasonString( + quoteStreamComplete?.reason, + )} /> - - ); - })() - : null} - - {insufficientNativeReserveError && !hasInsufficientBalance - ? (() => { - const bannerStyle = { - borderLeftWidth: 4, - borderColor: colors.warning.default, - backgroundColor: colors.warning.muted, - paddingLeft: 8, - }; - return ( - { + const isMalicious = + tokenWarning.type === SecurityDataType.Malicious; + const bannerColors = isMalicious + ? colors.error + : colors.warning; + const bannerStyle = { + borderLeftWidth: 4, + borderColor: bannerColors.default, + backgroundColor: bannerColors.muted, + paddingLeft: 8, + }; + const securityConfig = getBridgeTokenSecurityConfig( + tokenWarning.type, + ); + const navigateToModal = () => + navigation.navigate(Routes.BRIDGE.MODALS.ROOT, { + screen: Routes.BRIDGE.MODALS.TOKEN_WARNING_MODAL, + params: { + warningType: tokenWarning.type, + features: tokenWarning.metadata?.features ?? [], + mode: TokenWarningModalMode.Info, + location, + }, + }); + return ( + + + } + description={ + isMalicious + ? strings( + 'bridge.token_warning_malicious_banner', + { + token: destToken?.symbol, + }, + ) + : strings( + 'bridge.token_warning_suspicious_banner', + { + token: destToken?.symbol, + }, + ) + } + onClose={navigateToModal} + closeButtonProps={{ iconName: CLIconName.ArrowRight }} /> - } - title={strings( - 'bridge.insufficient_native_reserve_title', - { ticker }, - )} - style={bannerStyle} - actionButtonProps={{ - label: strings( - 'bridge.insufficient_native_reserve_cta', - ), - onPress: () => - handleSourcePresetAmountSelect( - insufficientNativeReserveError.maxSwappableNativeBalance, + + ); + })() + : null} + + {insufficientNativeReserveError && !hasInsufficientBalance + ? (() => { + const bannerStyle = { + borderLeftWidth: 4, + borderColor: colors.warning.default, + backgroundColor: colors.warning.muted, + paddingLeft: 8, + }; + return ( + + } + title={strings( + 'bridge.insufficient_native_reserve_title', + { ticker }, + )} + style={bannerStyle} + actionButtonProps={{ + label: strings( + 'bridge.insufficient_native_reserve_cta', ), - variant: ButtonVariants.Primary, - size: ButtonSize.Sm, - style: { - marginTop: 6, - }, - }} - description={strings( - 'bridge.insufficient_native_reserve_message', - { - ticker, - minimumReserve: - insufficientNativeReserveError.minimumNativeBalanceToBeKeptInAccount, - maxSwappable: - insufficientNativeReserveError.maxSwappableNativeBalance, - }, - )} - /> - ); - })() - : null} - - {contentMode === 'quote' && - activeQuote && - hasMissingPriceData(activeQuote) ? ( - - ) : null} - + onPress: () => + handleSourcePresetAmountSelect( + insufficientNativeReserveError.maxSwappableNativeBalance, + ), + variant: ButtonVariants.Primary, + size: ButtonSize.Sm, + style: { + marginTop: 6, + }, + }} + description={strings( + 'bridge.insufficient_native_reserve_message', + { + ticker, + minimumReserve: + insufficientNativeReserveError.minimumNativeBalanceToBeKeptInAccount, + maxSwappable: + insufficientNativeReserveError.maxSwappableNativeBalance, + }, + )} + /> + ); + })() + : null} + + {contentMode === 'quote' && + activeQuote && + hasMissingPriceData(activeQuote) ? ( + + ) : null} + - + {contentMode === 'loading' ? ( + + + + ) : null} + {contentMode === 'quote' ? ( + + + + ) : null} + {shouldShowTrendingTokens ? ( + + ) : null} + + + + + + - {contentMode === 'loading' ? ( - - - - ) : null} - {contentMode === 'quote' ? ( - - - - ) : null} - {shouldShowTrendingTokens ? ( - - ) : null} - - - - - - - {sourceAmount && sourceAmount !== '0' ? ( - - ) : ( - - )} - - - + {sourceAmount && sourceAmount !== '0' ? ( + + ) : ( + + )} + + + + ); }; diff --git a/app/components/UI/Bridge/components/BridgeTokenSelector/BridgeTokenSelector.test.tsx b/app/components/UI/Bridge/components/BridgeTokenSelector/BridgeTokenSelector.test.tsx index 80e81b5414e..5c241be8da8 100644 --- a/app/components/UI/Bridge/components/BridgeTokenSelector/BridgeTokenSelector.test.tsx +++ b/app/components/UI/Bridge/components/BridgeTokenSelector/BridgeTokenSelector.test.tsx @@ -230,12 +230,6 @@ jest.mock('../../hooks/useTokenSelection', () => ({ jest.mock('../../../../../../locales/i18n', () => ({ strings: (key: string) => key, })); -jest.mock( - '../../../../../component-library/components-temp/HeaderCompactStandard', - () => ({ - getHeaderCompactStandardNavbarOptions: jest.fn(() => ({})), - }), -); const mockTrackEvent = jest.fn(); jest.mock('../../../../../core/Engine', () => ({ @@ -298,13 +292,48 @@ jest.mock('@metamask/design-system-react-native', () => { Box: ({ children, style }: { children: React.ReactNode; style?: object }) => createElement(View, { style }, children), Text: 'Text', - ButtonIcon: ({ onPress }: { onPress?: () => void }) => - createElement(TouchableOpacity, { onPress, testID: 'button-icon-info' }), + ButtonIcon: ({ + onPress, + iconName, + }: { + onPress?: () => void; + iconName?: string; + }) => + createElement(TouchableOpacity, { + onPress, + // Derive the testID from the iconName so different ButtonIcons + // (e.g. Info on each row, ArrowLeft in the inline header) don't + // collide on the same selector. + testID: `button-icon-${String(iconName ?? 'unknown').toLowerCase()}`, + }), ButtonIconSize: { Md: 'Md' }, IconColor: { IconAlternative: 'IconAlternative' }, - IconName: { Info: 'Info', Check: 'Check' }, + IconName: { Info: 'Info', Check: 'Check', ArrowLeft: 'ArrowLeft' }, Icon: 'Icon', IconSize: { Md: 'Md' }, + HeaderStandard: ({ + title, + onBack, + }: { + title?: string; + onBack?: () => void; + }) => + createElement( + View, + { testID: 'header-standard' }, + // Render the title text so existing assertions on `getByText(title)` pass. + // Render the back button only when onBack is provided to mirror the + // real component's behaviour. + title + ? createElement('Text', { testID: 'header-standard-title' }, title) + : null, + onBack + ? createElement(TouchableOpacity, { + onPress: onBack, + testID: 'button-icon-arrowleft', + }) + : null, + ), TextVariant: { HeadingSm: 'HeadingSm', HeadingMd: 'HeadingMd', @@ -587,10 +616,15 @@ describe('BridgeTokenSelector', () => { }); describe('rendering', () => { - it('renders and sets navigation options', () => { - const { getByTestId } = renderWithReduxProvider(); + it('renders the search input and inline header title', () => { + const { getByTestId, getByText } = renderWithReduxProvider( + , + ); expect(getByTestId('bridge-token-search-input')).toBeTruthy(); - expect(mockSetOptions).toHaveBeenCalled(); + // Header is now inlined inside the screen instead of being set via + // navigation.setOptions, so assert on the rendered title instead. + // strings() is mocked to return the key. + expect(getByText('bridge.select_token')).toBeTruthy(); }); it('renders skeleton items during loading', async () => { diff --git a/app/components/UI/Bridge/components/BridgeTokenSelector/BridgeTokenSelector.tsx b/app/components/UI/Bridge/components/BridgeTokenSelector/BridgeTokenSelector.tsx index 825bb7ad8b1..146649da2d0 100644 --- a/app/components/UI/Bridge/components/BridgeTokenSelector/BridgeTokenSelector.tsx +++ b/app/components/UI/Bridge/components/BridgeTokenSelector/BridgeTokenSelector.tsx @@ -19,7 +19,6 @@ import { } from '@react-navigation/native'; import { useSelector, useDispatch } from 'react-redux'; import { strings } from '../../../../../../locales/i18n'; -import { getHeaderCompactStandardNavbarOptions } from '../../../../../component-library/components-temp/HeaderCompactStandard'; import { FlatList } from 'react-native-gesture-handler'; import { NetworkPills } from './NetworkPills'; import Routes from '../../../../../constants/navigation/Routes'; @@ -40,6 +39,7 @@ import { Box, ButtonIcon, ButtonIconSize, + HeaderStandard, IconColor, IconName, Text, @@ -105,17 +105,6 @@ export const BridgeTokenSelector: React.FC = () => { const enabledChainRanking = useSelector(selectAllowedChainRanking); - // Set navigation options for header - useEffect(() => { - navigation.setOptions( - getHeaderCompactStandardNavbarOptions({ - title: strings('bridge.select_token'), - onBack: () => navigation.goBack(), - includesTopInset: true, - }), - ); - }, [navigation]); - // Use custom hook for token selection const { handleTokenPress, selectedToken } = useTokenSelection( route.params?.type, @@ -525,6 +514,11 @@ export const BridgeTokenSelector: React.FC = () => { return ( + navigation.goBack()} + includesTopInset + /> { + const { theme } = params; + return StyleSheet.create({ + screenWrapper: { + flex: 1, + backgroundColor: theme.colors.background.default, + }, + }); +}; diff --git a/app/components/UI/Bridge/components/QuoteSelectorView/index.test.tsx b/app/components/UI/Bridge/components/QuoteSelectorView/index.test.tsx index 0b9a494c156..e9c8097ec2a 100644 --- a/app/components/UI/Bridge/components/QuoteSelectorView/index.test.tsx +++ b/app/components/UI/Bridge/components/QuoteSelectorView/index.test.tsx @@ -210,16 +210,10 @@ describe('QuoteSelectorView', () => { }); describe('navigation setup', () => { - it('sets navigation options on mount', () => { - render(); - - expect(mockSetOptions).toHaveBeenCalled(); - }); - - it('calls goBack when back action is triggered', () => { - render(); + it('renders inline header with the screen title', () => { + const { getByText } = render(); - expect(mockSetOptions).toHaveBeenCalled(); + expect(getByText(strings('bridge.select_quote'))).toBeTruthy(); }); }); diff --git a/app/components/UI/Bridge/components/QuoteSelectorView/index.tsx b/app/components/UI/Bridge/components/QuoteSelectorView/index.tsx index 1813e8d0a54..bdd52d1c29b 100644 --- a/app/components/UI/Bridge/components/QuoteSelectorView/index.tsx +++ b/app/components/UI/Bridge/components/QuoteSelectorView/index.tsx @@ -1,14 +1,17 @@ import ScreenView from '../../../../Base/ScreenView'; import { Box, + HeaderStandard, Text, TextColor, TextVariant, } from '@metamask/design-system-react-native'; import React, { useCallback, useEffect, useMemo } from 'react'; +import { SafeAreaView } from 'react-native-safe-area-context'; import { strings } from '../../../../../../locales/i18n'; import { useNavigation } from '@react-navigation/native'; -import { getHeaderCompactStandardNavbarOptions } from '../../../../../component-library/components-temp/HeaderCompactStandard'; +import { useStyles } from '../../../../../component-library/hooks'; +import { createStyles } from './QuoteSelectorView.styles'; import { useDispatch, useSelector } from 'react-redux'; import { selectDestToken, @@ -30,6 +33,7 @@ import { useTrackAllQuotesSortedEvent } from '../../hooks/useTrackAllQuotesSorte import { fromTokenMinimalUnit } from '../../../../../util/number'; export const QuoteSelectorView = () => { + const { styles } = useStyles(createStyles, {}); const navigation = useNavigation(); const dispatch = useDispatch(); const selectedQuoteRequestId = useSelector(selectSelectedQuoteRequestId); @@ -112,16 +116,6 @@ export const QuoteSelectorView = () => { destToken, ]); - useEffect(() => { - navigation.setOptions( - getHeaderCompactStandardNavbarOptions({ - title: strings('bridge.select_quote'), - onBack: () => navigation.goBack(), - includesTopInset: true, - }), - ); - }, [navigation]); - // Go back to bridge view only if there's an error or quotes are expired useEffect(() => { if (quoteFetchError || blockaidError) { @@ -130,13 +124,23 @@ export const QuoteSelectorView = () => { }, [quoteFetchError, blockaidError, navigation]); return ( - - - - {strings('bridge.select_quote_info')} - - - - + + navigation.goBack()} + includesTopInset + /> + + + + {strings('bridge.select_quote_info')} + + + + + ); }; diff --git a/app/components/UI/Bridge/routes.tsx b/app/components/UI/Bridge/routes.tsx index 58e11e90cc9..eabc77826ad 100644 --- a/app/components/UI/Bridge/routes.tsx +++ b/app/components/UI/Bridge/routes.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { createStackNavigator } from '@react-navigation/stack'; +import { createNativeStackNavigator } from '@react-navigation/native-stack'; import Routes from '../../../constants/navigation/Routes'; import { BridgeTokenSelector } from './components/BridgeTokenSelector'; import BridgeView from './Views/BridgeView'; @@ -12,44 +12,37 @@ import { CustomSlippageModal } from './components/SlippageModal/CustomSlippageMo import NetworkListModal from './components/BridgeTokenSelector/NetworkListModal'; import { QuoteSelectorView } from './components/QuoteSelectorView'; import { PriceImpactModal } from './components/PriceImpactModal'; -import { clearStackNavigatorOptions } from '../../../constants/navigation/clearStackNavigatorOptions'; +import { + clearNativeStackNavigatorOptions, + transparentModalScreenOptions, +} from '../../../constants/navigation/clearStackNavigatorOptions'; import { TokenWarningModal } from './components/TokenWarningModal'; import { MissingPriceModal } from './components/MissingPriceModal'; // eslint-disable-next-line @typescript-eslint/no-explicit-any type ScreenComponent = React.ComponentType; -const Stack = createStackNavigator(); +const Stack = createNativeStackNavigator(); export const BridgeScreenStack = () => ( - - + + ); -const ModalStack = createStackNavigator(); +const ModalStack = createNativeStackNavigator(); export const BridgeModalStack = () => ( { createOptimisticPositionFromPreview: jest.fn(), clearOptimisticPosition: jest.fn(), getCryptoTargetPrice: jest.fn(), + invalidateAccountState: jest.fn(), + beforePublishDepositWalletDeposit: jest.fn(), + syncDepositWalletBalanceAllowanceForDepositTransaction: jest.fn(), } as unknown as jest.Mocked; + mockPolymarketProvider.beforePublishDepositWalletDeposit.mockResolvedValue( + true, + ); + mockPolymarketProvider.syncDepositWalletBalanceAllowanceForDepositTransaction.mockResolvedValue( + undefined, + ); + // Default safe mocks for async fire-and-forget methods // (prevents unhandled rejections when payWithAnyTokenConfirmation is // triggered by onBuyPaymentTokenChange but the async chain completes @@ -4885,6 +4895,38 @@ describe('PredictController', () => { }); }); + it('invalidates account state and syncs deposit-wallet balance allowance after confirmed deposits', async () => { + await withController(async ({ controller, messenger }) => { + const transactionMeta = createPredictTransactionMeta({ + nestedType: TransactionType.predictDeposit, + status: TransactionStatus.confirmed, + from: accountAddress, + }); + + controller.updateStateForTesting((state) => { + state.pendingDeposits = { + [accountAddress]: 'pending', + }; + }); + + messenger.publish('TransactionController:transactionStatusUpdated', { + transactionMeta, + } as { transactionMeta: TransactionMeta }); + + await Promise.resolve(); + + expect( + mockPolymarketProvider.invalidateAccountState, + ).toHaveBeenCalledWith(accountAddress); + expect( + mockPolymarketProvider.syncDepositWalletBalanceAllowanceForDepositTransaction, + ).toHaveBeenCalledWith({ + transactionMeta, + signerAddress: accountAddress, + }); + }); + }); + it('clears only sender pending deposit when selected account differs', () => { withController(({ controller, messenger }) => { const selectedAddress = accountAddress; @@ -5721,6 +5763,7 @@ describe('PredictController', () => { const mockAccountState = { address: '0xProxyAddress' as `0x${string}`, isDeployed: true, + walletType: 'safe' as const, balance: 100.5, }; @@ -6139,6 +6182,46 @@ describe('PredictController', () => { }); }); + describe('beforePublish', () => { + it('delegates to provider deposit-wallet preflight', async () => { + await withController(async ({ controller }) => { + const transactionMeta = { + id: 'tx-before-publish', + txParams: { + from: '0x1234567890123456789012345678901234567890', + }, + } as TransactionMeta; + + const result = await controller.beforePublish({ transactionMeta }); + + expect(result).toBe(true); + expect( + mockPolymarketProvider.beforePublishDepositWalletDeposit, + ).toHaveBeenCalledWith({ + transactionMeta, + getSigner: expect.any(Function), + }); + }); + }); + }); + + describe('publish', () => { + it('passes through by default', async () => { + await withController(async ({ controller }) => { + const result = await controller.publish({ + transactionMeta: { + id: 'tx-1', + txParams: { + from: MOCK_ADDRESS, + }, + } as TransactionMeta, + }); + + expect(result).toEqual({ transactionHash: undefined }); + }); + }); + }); + describe('beforeSign', () => { const mockTransactionMeta = { id: 'tx-1', @@ -9043,8 +9126,8 @@ describe('PredictController', () => { ], }) as any; - it('places the order when depositAndOrder transaction is confirmed and preview exists', () => { - withController(({ controller, messenger }) => { + it('places the order when depositAndOrder transaction is confirmed and preview exists', async () => { + await withController(async ({ controller, messenger }) => { const preview = createMockOrderPreview(); const placeOrderSpy = jest .spyOn(controller, 'placeOrder') @@ -9092,6 +9175,9 @@ describe('PredictController', () => { }, } as { transactionMeta: TransactionMeta }); + await Promise.resolve(); + await Promise.resolve(); + expect(placeOrderSpy).toHaveBeenCalledWith({ analyticsProperties: { marketId: 'market-1' }, preview, @@ -9101,8 +9187,78 @@ describe('PredictController', () => { }); }); - it('forwards activeAbTests to placeOrder when depositAndOrder is confirmed', () => { - withController(({ controller, messenger }) => { + it('waits for deposit-wallet balance allowance sync before placing depositAndOrder', async () => { + await withController(async ({ controller, messenger }) => { + const preview = createMockOrderPreview(); + const placeOrderSpy = jest + .spyOn(controller, 'placeOrder') + .mockResolvedValue({ + success: true, + response: { + id: 'order-123', + spentAmount: '100', + receivedAmount: '200', + }, + } as any); + let resolveSync: () => void = jest.fn(); + const syncPromise = new Promise((resolve) => { + resolveSync = resolve; + }); + mockPolymarketProvider.syncDepositWalletBalanceAllowanceForDepositTransaction.mockReturnValueOnce( + syncPromise, + ); + + setActiveOrderForTest(controller, { + state: ActiveOrderState.DEPOSITING, + transactionId: 'tx-1', + }); + ( + controller as unknown as { + pendingOrderPreviews: { + [transactionId: string]: { + preview: OrderPreview; + signerAddress: string; + }; + }; + } + ).pendingOrderPreviews['tx-1'] = { + preview, + signerAddress: accountAddress, + }; + + messenger.publish('TransactionController:transactionStatusUpdated', { + transactionMeta: { + ...createPredictTransactionMeta({ + nestedType: TransactionType.predictDeposit, + status: TransactionStatus.confirmed, + }), + type: TransactionType.predictDepositAndOrder, + nestedTransactions: [ + { type: TransactionType.predictDepositAndOrder }, + ], + }, + } as { transactionMeta: TransactionMeta }); + + await Promise.resolve(); + + expect(placeOrderSpy).not.toHaveBeenCalled(); + + resolveSync(); + await Promise.resolve(); + await Promise.resolve(); + + expect(placeOrderSpy).toHaveBeenCalledWith( + expect.objectContaining({ + preview, + address: accountAddress, + transactionId: 'tx-1', + }), + ); + }); + }); + + it('forwards activeAbTests to placeOrder when depositAndOrder is confirmed', async () => { + await withController(async ({ controller, messenger }) => { const preview = createMockOrderPreview(); const abTests = [ { key: 'predict-pwat-experiment', value: 'treatment' }, @@ -9150,6 +9306,9 @@ describe('PredictController', () => { }, } as { transactionMeta: TransactionMeta }); + await Promise.resolve(); + await Promise.resolve(); + expect(placeOrderSpy).toHaveBeenCalledWith( expect.objectContaining({ activeAbTests: abTests }), ); @@ -9611,8 +9770,8 @@ describe('PredictController', () => { }); }); - it('does not update activeBuyOrder when deposit confirms for a different active order', () => { - withController(({ controller, messenger }) => { + it('does not update activeBuyOrder when deposit confirms for a different active order', async () => { + await withController(async ({ controller, messenger }) => { setActiveOrderForTest(controller, { state: ActiveOrderState.PREVIEW, }); @@ -9660,6 +9819,9 @@ describe('PredictController', () => { }, } as { transactionMeta: TransactionMeta }); + await Promise.resolve(); + await Promise.resolve(); + expect(placeOrderSpy).toHaveBeenCalledWith({ analyticsProperties: { marketId: 'market-1' }, preview, @@ -9771,8 +9933,8 @@ describe('PredictController', () => { ], }) as any; - it('forwards the transaction address to placeOrder when depositAndOrder confirms after account switch', () => { - withController(({ controller, messenger }) => { + it('forwards the transaction address to placeOrder when depositAndOrder confirms after account switch', async () => { + await withController(async ({ controller, messenger }) => { const preview = createMockOrderPreview(); const placeOrderSpy = jest .spyOn(controller, 'placeOrder') @@ -9820,6 +9982,9 @@ describe('PredictController', () => { }, } as { transactionMeta: TransactionMeta }); + await Promise.resolve(); + await Promise.resolve(); + expect(placeOrderSpy).toHaveBeenCalledWith({ analyticsProperties: { marketId: 'market-2' }, preview, diff --git a/app/components/UI/Predict/controllers/PredictController.ts b/app/components/UI/Predict/controllers/PredictController.ts index 4396fb809e1..c85e227de6a 100644 --- a/app/components/UI/Predict/controllers/PredictController.ts +++ b/app/components/UI/Predict/controllers/PredictController.ts @@ -345,6 +345,7 @@ export interface PredictControllerOptions { } const MESSENGER_EXPOSED_METHODS = [ + 'beforePublish', 'beforeSign', 'claimWithConfirmation', 'clearActiveOrder', @@ -370,6 +371,7 @@ const MESSENGER_EXPOSED_METHODS = [ 'onPlaceOrderSuccess', 'placeOrder', 'prepareWithdraw', + 'publish', 'previewOrder', 'refreshEligibility', 'selectPaymentToken', @@ -2142,6 +2144,38 @@ export class PredictController extends BaseController< }); } + private async syncDepositWalletBalanceAllowanceIfNeeded({ + transactionMeta, + address, + }: { + transactionMeta: TransactionMeta; + address: string; + }): Promise { + try { + await this.provider.syncDepositWalletBalanceAllowanceForDepositTransaction( + { + transactionMeta, + signerAddress: address, + }, + ); + } catch (error) { + DevLogger.log( + 'PredictController: Deposit wallet balance-allowance sync failed', + { + error: error instanceof Error ? error.message : 'Unknown error', + transactionId: transactionMeta.id, + }, + ); + Logger.error( + ensureError(error), + this.getErrorContext('syncDepositWalletBalanceAllowanceIfNeeded', { + operation: 'deposit_wallet_balance_allowance_sync', + transactionId: transactionMeta.id, + }), + ); + } + } + private handleTransactionSideEffects( type: PredictTransactionEventType, status: PredictTransactionEventStatus, @@ -2155,6 +2189,20 @@ export class PredictController extends BaseController< this.clearPendingDepositForAddress({ address }); } + let depositWalletSyncPromise: Promise | undefined; + if ( + (type === 'deposit' || type === 'depositAndOrder') && + status === 'confirmed' + ) { + this.provider.invalidateAccountState(address); + depositWalletSyncPromise = this.syncDepositWalletBalanceAllowanceIfNeeded( + { + transactionMeta, + address, + }, + ); + } + if (type === 'depositAndOrder' && status === 'confirmed') { const transactionId = transactionMeta.id; const pendingOrder = transactionId @@ -2184,20 +2232,24 @@ export class PredictController extends BaseController< activeAbTests: pendingActiveAbTests, } = pendingOrder; - this.placeOrder({ - analyticsProperties: pendingAnalytics, - activeAbTests: pendingActiveAbTests, - preview, - address: signerAddress, - transactionId, - }).catch((error) => { - Logger.error( - ensureError(error), - this.getErrorContext('handleTransactionSideEffects', { - operation: 'placeOrder', + (depositWalletSyncPromise ?? Promise.resolve()) + .then(() => + this.placeOrder({ + analyticsProperties: pendingAnalytics, + activeAbTests: pendingActiveAbTests, + preview, + address: signerAddress, + transactionId, }), - ); - }); + ) + .catch((error) => { + Logger.error( + ensureError(error), + this.getErrorContext('handleTransactionSideEffects', { + operation: 'placeOrder', + }), + ); + }); } if (type === 'depositAndOrder' && status === 'failed') { @@ -2619,6 +2671,15 @@ export class PredictController extends BaseController< } } + public async beforePublish(request: { + transactionMeta: TransactionMeta; + }): Promise { + return this.provider.beforePublishDepositWalletDeposit({ + transactionMeta: request.transactionMeta, + getSigner: (address?: string) => this.getSigner(address), + }); + } + public async beforeSign(request: { transactionMeta: TransactionMeta; }): Promise< @@ -2722,6 +2783,12 @@ export class PredictController extends BaseController< }; } + public async publish(_request: { + transactionMeta: TransactionMeta; + }): Promise<{ transactionHash?: string }> { + return { transactionHash: undefined }; + } + public clearWithdrawTransaction(): void { this.update((state) => { state.withdrawTransaction = null; @@ -2730,6 +2797,7 @@ export class PredictController extends BaseController< } export type { + PredictControllerBeforePublishAction, PredictControllerBeforeSignAction, PredictControllerClaimWithConfirmationAction, PredictControllerClearActiveOrderAction, @@ -2754,6 +2822,7 @@ export type { PredictControllerPlaceOrderAction, PredictControllerPrepareWithdrawAction, PredictControllerPreviewOrderAction, + PredictControllerPublishAction, PredictControllerRefreshEligibilityAction, PredictControllerSelectPaymentTokenAction, PredictControllerSetSelectedPaymentTokenAction, diff --git a/app/components/UI/Predict/hooks/usePredictAccountState.test.ts b/app/components/UI/Predict/hooks/usePredictAccountState.test.ts index 29bf6383312..d282222d359 100644 --- a/app/components/UI/Predict/hooks/usePredictAccountState.test.ts +++ b/app/components/UI/Predict/hooks/usePredictAccountState.test.ts @@ -63,6 +63,7 @@ describe('usePredictAccountState', () => { const mockAccountState = { address: '0x1234567890abcdef1234567890abcdef12345678', isDeployed: true, + walletType: 'safe' as const, }; beforeEach(() => { diff --git a/app/components/UI/Predict/providers/polymarket/PolymarketProvider.test.ts b/app/components/UI/Predict/providers/polymarket/PolymarketProvider.test.ts index 2e6bf89212e..f286c71639c 100644 --- a/app/components/UI/Predict/providers/polymarket/PolymarketProvider.test.ts +++ b/app/components/UI/Predict/providers/polymarket/PolymarketProvider.test.ts @@ -1,16 +1,35 @@ -import { CHAIN_IDS, TransactionType } from '@metamask/transaction-controller'; +import { + CHAIN_IDS, + TransactionType, + type TransactionMeta, +} from '@metamask/transaction-controller'; import { SignTypedDataVersion } from '@metamask/keyring-controller'; +import { Interface } from 'ethers/lib/utils'; +import { analytics } from '../../../../../util/analytics/analytics'; +import { UserProfileProperty } from '../../../../../util/metrics/UserSettingsAnalyticsMetaData/UserProfileAnalyticsMetaData.types'; import { DEFAULT_FEE_COLLECTION_FLAG } from '../../constants/flags'; import type { OrderPreview } from '../types'; import { Side } from '../../types'; import type { PredictFeatureFlags } from '../../types/flags'; import { PolymarketProvider } from './PolymarketProvider'; +import { OrderType, SignatureType } from './types'; +import { + deriveDepositWalletAddress, + executeDepositWalletBatch, + getDepositWalletRelayerTransactionId, + requestDepositWalletCreate, + syncDepositWalletCollateralBalanceAllowance, + toDepositWalletCalls, + waitForDepositWalletDeployed, + waitForDepositWalletTransaction, +} from './depositWallet'; import { DEFAULT_CLOB_BASE_URL, MATIC_CONTRACTS_V2, POLYMARKET_PROVIDER_ID, USDC_E_ADDRESS, } from './constants'; +import { OperationType } from './safe/types'; import { computeProxyAddress, createPermit2FeeAuthorization, @@ -29,6 +48,9 @@ import { } from './utils'; import { submitProtocolClobOrder } from './protocol/transport'; import { buildDepositMaintenanceTransaction } from './preflight/deposit'; +import type { SignedSafeExecution } from './preflight/core'; +import { planDepositWalletPreflight } from './preflight/depositWallet'; +import { buildLegacySafeMigrationSweepTransaction } from './preflight/legacySafeMigration'; import { buildTradeAllowancesTx } from './preflight/trade'; import { buildWithdrawTransaction } from './preflight/withdraw'; import { @@ -93,10 +115,29 @@ jest.mock('./protocol/transport', () => ({ submitProtocolClobOrder: jest.fn(), })); +jest.mock('./depositWallet', () => ({ + deriveDepositWalletAddress: jest.fn(), + executeDepositWalletBatch: jest.fn(), + getDepositWalletRelayerTransactionId: jest.fn(), + requestDepositWalletCreate: jest.fn(), + syncDepositWalletCollateralBalanceAllowance: jest.fn(), + toDepositWalletCalls: jest.fn(), + waitForDepositWalletDeployed: jest.fn(), + waitForDepositWalletTransaction: jest.fn(), +})); + jest.mock('./preflight/deposit', () => ({ buildDepositMaintenanceTransaction: jest.fn(), })); +jest.mock('./preflight/depositWallet', () => ({ + planDepositWalletPreflight: jest.fn(), +})); + +jest.mock('./preflight/legacySafeMigration', () => ({ + buildLegacySafeMigrationSweepTransaction: jest.fn(), +})); + jest.mock('./preflight/trade', () => ({ buildTradeAllowancesTx: jest.fn(), })); @@ -105,8 +146,14 @@ jest.mock('./preflight/withdraw', () => ({ buildWithdrawTransaction: jest.fn(), })); +const mockAnalyticsIdentify = jest.mocked(analytics.identify); const mockComputeProxyAddress = jest.mocked(computeProxyAddress); const mockCreateApiKey = jest.mocked(createApiKey); +const mockDeriveDepositWalletAddress = jest.mocked(deriveDepositWalletAddress); +const mockExecuteDepositWalletBatch = jest.mocked(executeDepositWalletBatch); +const mockGetDepositWalletRelayerTransactionId = jest.mocked( + getDepositWalletRelayerTransactionId, +); const mockCreatePermit2FeeAuthorization = jest.mocked( createPermit2FeeAuthorization, ); @@ -127,15 +174,63 @@ const mockSubmitProtocolClobOrder = jest.mocked(submitProtocolClobOrder); const mockBuildDepositMaintenanceTransaction = jest.mocked( buildDepositMaintenanceTransaction, ); +const mockPlanDepositWalletPreflight = jest.mocked(planDepositWalletPreflight); +const mockBuildLegacySafeMigrationSweepTransaction = jest.mocked( + buildLegacySafeMigrationSweepTransaction, +); +const mockRequestDepositWalletCreate = jest.mocked(requestDepositWalletCreate); +const mockSyncDepositWalletCollateralBalanceAllowance = jest.mocked( + syncDepositWalletCollateralBalanceAllowance, +); +const mockToDepositWalletCalls = jest.mocked(toDepositWalletCalls); +const mockWaitForDepositWalletDeployed = jest.mocked( + waitForDepositWalletDeployed, +); +const mockWaitForDepositWalletTransaction = jest.mocked( + waitForDepositWalletTransaction, +); const mockBuildTradeAllowancesTx = jest.mocked(buildTradeAllowancesTx); const mockBuildWithdrawTransaction = jest.mocked(buildWithdrawTransaction); +const depositWalletAddress = '0x2222222222222222222222222222222222222222'; +const legacySafeAddress = '0x9999999999999999999999999999999999999999'; const signer = { address: '0x1111111111111111111111111111111111111111', signPersonalMessage: jest.fn(), signTypedMessage: jest.fn(), }; +const erc20Interface = new Interface([ + 'function transfer(address to, uint256 value)', +]); + +function createDepositTransactionMeta({ + recipient, + tokenAddress = MATIC_CONTRACTS_V2.collateral, + type = TransactionType.predictDeposit, +}: { + recipient: string; + tokenAddress?: string; + type?: TransactionType; +}): TransactionMeta { + return { + id: 'tx-1', + txParams: { + from: signer.address, + to: tokenAddress, + value: '0x0', + data: '0x', + }, + nestedTransactions: [ + { + type, + to: tokenAddress, + data: erc20Interface.encodeFunctionData('transfer', [recipient, 0]), + }, + ], + } as TransactionMeta; +} + const basePreview: OrderPreview = { marketId: 'market-1', outcomeId: @@ -192,9 +287,8 @@ describe('PolymarketProvider', () => { beforeEach(() => { jest.clearAllMocks(); - mockComputeProxyAddress.mockReturnValue( - '0x9999999999999999999999999999999999999999', - ); + mockComputeProxyAddress.mockReturnValue(legacySafeAddress); + mockDeriveDepositWalletAddress.mockReturnValue(depositWalletAddress); mockCreateApiKey.mockResolvedValue({ apiKey: 'api-key', secret: 'secret', @@ -229,6 +323,34 @@ describe('PolymarketProvider', () => { type: TransactionType.contractInteraction, }); mockBuildDepositMaintenanceTransaction.mockResolvedValue(undefined); + mockBuildLegacySafeMigrationSweepTransaction.mockResolvedValue(undefined); + mockGetDepositWalletRelayerTransactionId.mockImplementation( + (response) => response.transactionID ?? response.id, + ); + mockRequestDepositWalletCreate.mockResolvedValue({ + transactionID: 'create-1', + }); + mockPlanDepositWalletPreflight.mockResolvedValue({ + missingRequirements: [], + transactions: [], + }); + mockToDepositWalletCalls.mockImplementation((transactions) => + transactions.map((transaction) => ({ + target: transaction.to, + value: transaction.value, + data: transaction.data, + })), + ); + mockExecuteDepositWalletBatch.mockResolvedValue({ + transactionID: 'batch-1', + }); + mockWaitForDepositWalletDeployed.mockResolvedValue(undefined); + mockWaitForDepositWalletTransaction.mockResolvedValue( + '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + ); + mockSyncDepositWalletCollateralBalanceAllowance.mockResolvedValue( + undefined, + ); mockEncodeErc20Transfer.mockReturnValue('0xtransfer'); mockGetRawBalance.mockResolvedValue(0n); mockGetSafeTransferAmount.mockReturnValue(1); @@ -242,7 +364,7 @@ describe('PolymarketProvider', () => { }); global.fetch = jest.fn().mockResolvedValue({ ok: true, - json: jest.fn().mockResolvedValue([]), + json: jest.fn().mockResolvedValue([{}]), }); signer.signTypedMessage.mockResolvedValue('0xsigned-order'); }); @@ -251,6 +373,79 @@ describe('PolymarketProvider', () => { expect(createProvider().providerId).toBe(POLYMARKET_PROVIDER_ID); }); + it('routes to deposit wallet when legacy Safe is not deployed', async () => { + mockIsSmartContractAddress + .mockResolvedValueOnce(false) + .mockResolvedValueOnce(true); + + const accountState = await createProvider().getAccountState({ + ownerAddress: signer.address, + }); + + expect(accountState).toEqual({ + address: depositWalletAddress, + isDeployed: true, + walletType: 'deposit-wallet', + }); + expect(global.fetch).not.toHaveBeenCalled(); + expect(mockIsSmartContractAddress).toHaveBeenNthCalledWith( + 1, + legacySafeAddress, + '0x89', + ); + expect(mockIsSmartContractAddress).toHaveBeenNthCalledWith( + 2, + depositWalletAddress, + '0x89', + ); + }); + + it('keeps legacy Safe users only when raw Activity API has activity', async () => { + const accountState = await createProvider().getAccountState({ + ownerAddress: signer.address, + }); + + expect(accountState).toEqual({ + address: legacySafeAddress, + isDeployed: true, + walletType: 'safe', + }); + expect(global.fetch).toHaveBeenCalledWith( + `https://data-api.polymarket.com/activity?user=${legacySafeAddress}&limit=1`, + ); + }); + + it('routes deployed legacy Safe with empty raw activity to deposit wallet', async () => { + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + json: jest.fn().mockResolvedValue([]), + }); + mockIsSmartContractAddress + .mockResolvedValueOnce(true) + .mockResolvedValueOnce(false); + + const accountState = await createProvider().getAccountState({ + ownerAddress: signer.address, + }); + + expect(accountState).toEqual({ + address: depositWalletAddress, + isDeployed: false, + walletType: 'deposit-wallet', + }); + }); + + it('fails closed when raw Activity API fails for a deployed legacy Safe', async () => { + global.fetch = jest.fn().mockResolvedValue({ + ok: false, + json: jest.fn(), + }); + + await expect( + createProvider().getAccountState({ ownerAddress: signer.address }), + ).rejects.toThrow('Failed to fetch Polymarket activity'); + }); + it('previews orders through canonical CLOB v2 with zero fee-rate bps', async () => { const provider = createProvider(); @@ -277,6 +472,9 @@ describe('PolymarketProvider', () => { expect.any(Object), SignTypedDataVersion.V4, ); + expect(mockGetL2Headers).toHaveBeenCalledWith( + expect.objectContaining({ address: signer.address }), + ); expect(mockSubmitProtocolClobOrder).toHaveBeenCalledWith( expect.objectContaining({ protocol: expect.objectContaining({ @@ -286,6 +484,13 @@ describe('PolymarketProvider', () => { clobVersionHeader: '2', }), }), + clobOrder: expect.objectContaining({ + order: expect.objectContaining({ + maker: legacySafeAddress, + signer: signer.address, + signatureType: SignatureType.POLY_GNOSIS_SAFE, + }), + }), allowancesTx: { to: '0x9999999999999999999999999999999999999999', data: '0xallowances', @@ -294,6 +499,177 @@ describe('PolymarketProvider', () => { ); }); + it('submits deposit-wallet orders with POLY_1271 payload and no Safe trade preflight fields', async () => { + const innerSignature = `0x${'11'.repeat(65)}`; + signer.signTypedMessage.mockResolvedValueOnce(innerSignature); + mockIsSmartContractAddress + .mockResolvedValueOnce(false) + .mockResolvedValueOnce(true); + + const provider = createProvider({ + fakOrdersEnabled: true, + feeCollection: { + ...DEFAULT_FEE_COLLECTION_FLAG, + permit2Enabled: true, + executors: ['0x4444444444444444444444444444444444444444'], + }, + }); + + const result = await provider.placeOrder({ + signer, + preview: { + ...basePreview, + fees: { + metamaskFee: 0.05, + providerFee: 0.05, + totalFee: 0.1, + totalFeePercentage: 1, + collector: '0x3333333333333333333333333333333333333333', + executors: ['0x4444444444444444444444444444444444444444'], + permit2Enabled: true, + }, + }, + }); + + expect(result.success).toBe(true); + expect(mockCreateApiKey).toHaveBeenCalledWith({ address: signer.address }); + expect(mockBuildTradeAllowancesTx).not.toHaveBeenCalled(); + expect(mockCreatePermit2FeeAuthorization).not.toHaveBeenCalled(); + expect(mockGetL2Headers).toHaveBeenCalledWith( + expect.objectContaining({ address: signer.address }), + ); + expect(signer.signTypedMessage).toHaveBeenCalledWith( + { + from: signer.address, + data: expect.objectContaining({ + primaryType: 'TypedDataSign', + message: expect.objectContaining({ + verifyingContract: depositWalletAddress, + }), + }), + }, + SignTypedDataVersion.V4, + ); + expect(mockSubmitProtocolClobOrder).toHaveBeenCalledWith( + expect.objectContaining({ + clobOrder: expect.objectContaining({ + orderType: OrderType.FAK, + order: expect.objectContaining({ + maker: depositWalletAddress, + signer: depositWalletAddress, + signatureType: SignatureType.POLY_1271, + signature: expect.stringMatching(/^0x11+/u), + }), + }), + feeAuthorization: undefined, + executor: undefined, + allowancesTx: undefined, + }), + ); + }); + + it('runs deposit-wallet setup before submitting deposit-wallet orders', async () => { + const repairTransaction = { + to: '0x4444444444444444444444444444444444444444', + data: '0xrepair', + operation: OperationType.Call, + value: '0', + }; + mockIsSmartContractAddress + .mockResolvedValueOnce(false) + .mockResolvedValueOnce(false) + .mockResolvedValueOnce(false) + .mockResolvedValueOnce(false); + mockPlanDepositWalletPreflight.mockResolvedValue({ + missingRequirements: [ + { + type: 'erc20-allowance', + tokenAddress: repairTransaction.to, + spender: '0x5555555555555555555555555555555555555555', + }, + ], + transactions: [repairTransaction], + }); + + const result = await createProvider().placeOrder({ + signer, + preview: basePreview, + }); + + expect(result.success).toBe(true); + expect(mockRequestDepositWalletCreate).toHaveBeenCalledWith({ + ownerAddress: signer.address, + }); + expect(mockExecuteDepositWalletBatch).toHaveBeenCalledWith({ + signer, + walletAddress: depositWalletAddress, + calls: [ + { + target: repairTransaction.to, + data: repairTransaction.data, + value: repairTransaction.value, + }, + ], + }); + expect( + mockSyncDepositWalletCollateralBalanceAllowance, + ).toHaveBeenCalledWith({ + protocol: expect.objectContaining({ key: 'v2' }), + signerAddress: signer.address, + apiKey: expect.objectContaining({ apiKey: 'api-key' }), + }); + expect( + mockExecuteDepositWalletBatch.mock.invocationCallOrder[0], + ).toBeLessThan(mockSubmitProtocolClobOrder.mock.invocationCallOrder[0]); + }); + + it('passes legacy Safe migration sweep as allowancesTx for deposit-wallet orders', async () => { + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + json: jest.fn().mockResolvedValue([]), + }); + mockIsSmartContractAddress + .mockResolvedValueOnce(true) + .mockResolvedValueOnce(true) + .mockResolvedValueOnce(true) + .mockResolvedValueOnce(true); + const sweepTransaction: SignedSafeExecution = { + params: { + to: legacySafeAddress as `0x${string}`, + data: '0xsweep' as `0x${string}`, + }, + type: TransactionType.contractInteraction, + }; + mockBuildLegacySafeMigrationSweepTransaction.mockResolvedValue( + sweepTransaction, + ); + + const result = await createProvider().placeOrder({ + signer, + preview: basePreview, + }); + + expect(result.success).toBe(true); + expect(mockBuildLegacySafeMigrationSweepTransaction).toHaveBeenCalledWith({ + signer, + legacySafeAddress, + depositWalletAddress, + protocol: expect.objectContaining({ key: 'v2' }), + }); + expect(mockSubmitProtocolClobOrder).toHaveBeenCalledWith( + expect.objectContaining({ + clobOrder: expect.objectContaining({ + order: expect.objectContaining({ + maker: depositWalletAddress, + signer: depositWalletAddress, + signatureType: SignatureType.POLY_1271, + }), + }), + allowancesTx: sweepTransaction.params, + }), + ); + }); + it('uses pUSD Permit2 fee authorization when fees are present', async () => { mockCreatePermit2FeeAuthorization.mockResolvedValue({ type: 'safe-permit2', @@ -341,7 +717,6 @@ describe('PolymarketProvider', () => { }); it('prepares pUSD deposits and optional legacy sweep maintenance', async () => { - mockIsSmartContractAddress.mockResolvedValue(false); mockBuildDepositMaintenanceTransaction.mockResolvedValue({ params: { to: '0x9999999999999999999999999999999999999999', @@ -353,10 +728,8 @@ describe('PolymarketProvider', () => { const result = await createProvider().prepareDeposit({ signer }); expect(result.chainId).toBe(CHAIN_IDS.POLYGON); + expect(mockGetDeployProxyWalletTransaction).not.toHaveBeenCalled(); expect(result.transactions).toEqual([ - expect.objectContaining({ - params: { to: '0xFactory', data: '0xdeploy' }, - }), { params: { to: MATIC_CONTRACTS_V2.collateral, @@ -373,6 +746,49 @@ describe('PolymarketProvider', () => { ]); }); + it('prepares deposit-wallet deposits with optional legacy Safe sweep first', async () => { + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + json: jest.fn().mockResolvedValue([]), + }); + mockIsSmartContractAddress + .mockResolvedValueOnce(true) + .mockResolvedValueOnce(false) + .mockResolvedValueOnce(true); + const sweepTransaction: SignedSafeExecution = { + params: { + to: legacySafeAddress, + data: '0xsweep', + }, + type: TransactionType.contractInteraction, + }; + mockBuildLegacySafeMigrationSweepTransaction.mockResolvedValue( + sweepTransaction, + ); + + const result = await createProvider().prepareDeposit({ signer }); + + expect(result.chainId).toBe(CHAIN_IDS.POLYGON); + expect(mockBuildLegacySafeMigrationSweepTransaction).toHaveBeenCalledWith({ + signer, + legacySafeAddress, + depositWalletAddress, + protocol: expect.objectContaining({ key: 'v2' }), + }); + expect(result.transactions).toEqual([ + sweepTransaction, + { + params: { + to: MATIC_CONTRACTS_V2.collateral, + data: '0xtransferData', + }, + type: TransactionType.predictDeposit, + }, + ]); + expect(mockRequestDepositWalletCreate).not.toHaveBeenCalled(); + expect(mockPlanDepositWalletPreflight).not.toHaveBeenCalled(); + }); + it('reads displayed Predict balance from pUSD plus legacy USDC.e', async () => { mockGetBalance.mockResolvedValue(12.5); mockGetRawBalance.mockResolvedValue(2_500_000n); @@ -405,6 +821,182 @@ describe('PolymarketProvider', () => { expect(mockGetRawBalance).toHaveBeenCalledTimes(1); }); + it('runs deposit-wallet beforePublish deployment and allowance preflight', async () => { + const provider = createProvider(); + const repairTransaction = { + to: '0x4444444444444444444444444444444444444444', + data: '0xrepair', + operation: OperationType.Call, + value: '0', + }; + mockIsSmartContractAddress.mockResolvedValueOnce(false); + mockPlanDepositWalletPreflight.mockResolvedValue({ + missingRequirements: [ + { + type: 'erc20-allowance', + tokenAddress: repairTransaction.to, + spender: '0x5555555555555555555555555555555555555555', + }, + ], + transactions: [repairTransaction], + }); + + const result = await provider.beforePublishDepositWalletDeposit({ + transactionMeta: createDepositTransactionMeta({ + recipient: depositWalletAddress, + }), + getSigner: () => signer, + }); + + expect(result).toBe(true); + expect(mockRequestDepositWalletCreate).toHaveBeenCalledWith({ + ownerAddress: signer.address, + }); + expect(mockWaitForDepositWalletTransaction).toHaveBeenNthCalledWith(1, { + transactionID: 'create-1', + requireCompletion: true, + }); + expect(mockWaitForDepositWalletDeployed).toHaveBeenCalledTimes(1); + expect(mockWaitForDepositWalletDeployed).toHaveBeenCalledWith({ + walletAddress: depositWalletAddress, + }); + expect(mockAnalyticsIdentify).toHaveBeenCalledWith({ + [UserProfileProperty.CREATED_POLYMARKET_ACCOUNT_VIA_MM]: true, + }); + expect( + mockSyncDepositWalletCollateralBalanceAllowance, + ).not.toHaveBeenCalled(); + expect(mockPlanDepositWalletPreflight).toHaveBeenCalledWith({ + walletAddress: depositWalletAddress, + protocol: expect.objectContaining({ key: 'v2' }), + }); + expect(mockExecuteDepositWalletBatch).toHaveBeenCalledWith({ + signer, + walletAddress: depositWalletAddress, + calls: [ + { + target: repairTransaction.to, + value: repairTransaction.value, + data: repairTransaction.data, + }, + ], + }); + expect(mockWaitForDepositWalletTransaction).toHaveBeenNthCalledWith(2, { + transactionID: 'batch-1', + requireCompletion: true, + }); + }); + + it('waits for WALLET-CREATE polling before submitting allowance batch', async () => { + const provider = createProvider(); + const repairTransaction = { + to: '0x4444444444444444444444444444444444444444', + data: '0xrepair', + operation: OperationType.Call, + value: '0', + }; + let resolveCreateWait: () => void = jest.fn(); + const createWaitPromise = new Promise<`0x${string}`>((resolve) => { + resolveCreateWait = () => + resolve( + '0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc', + ); + }); + mockIsSmartContractAddress.mockResolvedValueOnce(false); + mockPlanDepositWalletPreflight.mockResolvedValue({ + missingRequirements: [ + { + type: 'erc20-allowance', + tokenAddress: repairTransaction.to, + spender: '0x5555555555555555555555555555555555555555', + }, + ], + transactions: [repairTransaction], + }); + mockWaitForDepositWalletTransaction + .mockImplementationOnce(() => createWaitPromise) + .mockResolvedValueOnce( + '0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd', + ); + + const publishPromise = provider.beforePublishDepositWalletDeposit({ + transactionMeta: createDepositTransactionMeta({ + recipient: depositWalletAddress, + }), + getSigner: () => signer, + }); + + await Promise.resolve(); + + expect(mockRequestDepositWalletCreate).toHaveBeenCalled(); + expect(mockWaitForDepositWalletDeployed).not.toHaveBeenCalled(); + expect(mockExecuteDepositWalletBatch).not.toHaveBeenCalled(); + + resolveCreateWait(); + await publishPromise; + + expect(mockWaitForDepositWalletDeployed).toHaveBeenCalledWith({ + walletAddress: depositWalletAddress, + }); + expect(mockExecuteDepositWalletBatch).toHaveBeenCalled(); + const deployedCallOrder = + mockWaitForDepositWalletDeployed.mock.invocationCallOrder[0] ?? 0; + const executeCallOrder = + mockExecuteDepositWalletBatch.mock.invocationCallOrder[0] ?? 0; + expect(deployedCallOrder).toBeLessThan(executeCallOrder); + }); + + it('does not run deposit-wallet beforePublish for Safe deposits', async () => { + const result = await createProvider().beforePublishDepositWalletDeposit({ + transactionMeta: createDepositTransactionMeta({ + recipient: legacySafeAddress, + }), + getSigner: () => signer, + }); + + expect(result).toBe(true); + expect(mockRequestDepositWalletCreate).not.toHaveBeenCalled(); + expect(mockPlanDepositWalletPreflight).not.toHaveBeenCalled(); + }); + + it('syncs deposit-wallet CLOB balance allowance after matching deposits', async () => { + await createProvider().syncDepositWalletBalanceAllowanceForDepositTransaction( + { + transactionMeta: createDepositTransactionMeta({ + recipient: depositWalletAddress, + }), + signerAddress: signer.address, + }, + ); + + expect( + mockSyncDepositWalletCollateralBalanceAllowance, + ).toHaveBeenCalledWith({ + protocol: expect.objectContaining({ key: 'v2' }), + signerAddress: signer.address, + apiKey: { + apiKey: 'api-key', + secret: 'secret', + passphrase: 'passphrase', + }, + }); + }); + + it('skips CLOB sync for Safe deposits', async () => { + await createProvider().syncDepositWalletBalanceAllowanceForDepositTransaction( + { + transactionMeta: createDepositTransactionMeta({ + recipient: legacySafeAddress, + }), + signerAddress: signer.address, + }, + ); + + expect( + mockSyncDepositWalletCollateralBalanceAllowance, + ).not.toHaveBeenCalled(); + }); + it('prepares editable pUSD withdraw transfers', async () => { const result = await createProvider().prepareWithdraw({ signer }); diff --git a/app/components/UI/Predict/providers/polymarket/PolymarketProvider.ts b/app/components/UI/Predict/providers/polymarket/PolymarketProvider.ts index 6a692e26c61..422e3712053 100644 --- a/app/components/UI/Predict/providers/polymarket/PolymarketProvider.ts +++ b/app/components/UI/Predict/providers/polymarket/PolymarketProvider.ts @@ -2,9 +2,13 @@ import { SignTypedDataVersion, type TypedMessageParams, } from '@metamask/keyring-controller'; -import { CHAIN_IDS, TransactionType } from '@metamask/transaction-controller'; +import { + CHAIN_IDS, + TransactionType, + type TransactionMeta, +} from '@metamask/transaction-controller'; import { Hex, numberToHex } from '@metamask/utils'; -import { getAddress, parseUnits } from 'ethers/lib/utils'; +import { getAddress, Interface, parseUnits } from 'ethers/lib/utils'; import { DevLogger } from '../../../../../core/SDKConnect/utils/DevLogger'; import Logger, { type LoggerErrorOptions } from '../../../../../util/Logger'; import { analytics } from '../../../../../util/analytics/analytics'; @@ -69,7 +73,6 @@ import { import { computeProxyAddress, createPermit2FeeAuthorization, - getDeployProxyWalletTransaction, getSafeTransferAmount, getSafeTransferAmountRaw, } from './safe/utils'; @@ -77,6 +80,7 @@ import { Permit2FeeAuthorization } from './safe/types'; import { ApiKeyCreds, OrderType, + SignatureType, PolymarketApiActivity, PolymarketApiEvent, PolymarketApiTeam, @@ -109,7 +113,6 @@ import { GameCache } from './GameCache'; import { TeamsCache } from './TeamsCache'; import { WebSocketManager } from './WebSocketManager'; import { - getProtocolDepositTokenAddress, getProtocolWithdrawTokenAddress, POLYMARKET_V2_PROTOCOL, type PolymarketProtocolDefinition, @@ -117,15 +120,27 @@ import { import { buildProtocolUnsignedOrder, getPreviewFeeRateBpsForProtocol, - getProtocolOrderTypedData, getProtocolVerifyingContract, serializeProtocolRelayerOrder, + signProtocolOrder, } from './protocol/orderCodec'; import { submitProtocolClobOrder } from './protocol/transport'; import { buildClaimTransaction } from './preflight/claim'; import { buildDepositMaintenanceTransaction } from './preflight/deposit'; +import { planDepositWalletPreflight } from './preflight/depositWallet'; +import { buildLegacySafeMigrationSweepTransaction } from './preflight/legacySafeMigration'; import { buildTradeAllowancesTx } from './preflight/trade'; import { buildWithdrawTransaction } from './preflight/withdraw'; +import { + deriveDepositWalletAddress, + executeDepositWalletBatch, + getDepositWalletRelayerTransactionId, + requestDepositWalletCreate, + syncDepositWalletCollateralBalanceAllowance, + toDepositWalletCalls, + waitForDepositWalletDeployed, + waitForDepositWalletTransaction, +} from './depositWallet'; export type SignTypedMessageFn = ( params: TypedMessageParams, @@ -152,6 +167,10 @@ interface OptimisticPositionUpdate { positionId?: string; } +const ERC20_TRANSFER_INTERFACE = new Interface([ + 'function transfer(address to, uint256 value)', +]); + export class PolymarketProvider implements PredictProvider { readonly providerId = POLYMARKET_PROVIDER_ID; readonly name = 'Polymarket'; @@ -178,6 +197,38 @@ export class PolymarketProvider implements PredictProvider { this.#getFeatureFlags = getFeatureFlags; } + #getAccountStateCacheKey(ownerAddress: string): string { + return getAddress(ownerAddress).toLowerCase(); + } + + #getCachedAccountState(ownerAddress: string): AccountState | undefined { + return this.#accountStateByAddress.get( + this.#getAccountStateCacheKey(ownerAddress), + ); + } + + #setCachedAccountState( + ownerAddress: string, + accountState: AccountState, + ): void { + this.#accountStateByAddress.set( + this.#getAccountStateCacheKey(ownerAddress), + accountState, + ); + } + + public invalidateAccountState(ownerAddress: string): void { + try { + this.#accountStateByAddress.delete( + this.#getAccountStateCacheKey(ownerAddress), + ); + } catch (error) { + DevLogger.log('PolymarketProvider: Failed to invalidate account state', { + error: error instanceof Error ? error.message : 'Unknown error', + }); + } + } + #getSupportedLeagues(): PredictSportsLeague[] { const { liveSportsLeagues } = this.#getFeatureFlags(); return filterSupportedLeagues(liveSportsLeagues); @@ -362,12 +413,14 @@ export class PolymarketProvider implements PredictProvider { fakOrdersEnabled, permit2FeeReady, permit2AllowanceReady, + manualFeeCollectionSupported = true, }: { preview: OrderPreview; feeCollection: PredictFeatureFlags['feeCollection']; fakOrdersEnabled: boolean; permit2FeeReady: boolean; permit2AllowanceReady: boolean; + manualFeeCollectionSupported?: boolean; }): OrderType { if ( !this.#shouldUseFakOrderType({ @@ -381,7 +434,11 @@ export class PolymarketProvider implements PredictProvider { const hasFees = preview.fees !== undefined && preview.fees.totalFee > 0; - if (!hasFees || (permit2FeeReady && permit2AllowanceReady)) { + if ( + !hasFees || + !manualFeeCollectionSupported || + (permit2FeeReady && permit2AllowanceReady) + ) { return OrderType.FAK; } @@ -422,9 +479,26 @@ export class PolymarketProvider implements PredictProvider { preview: OrderPreview; protocol: PolymarketProtocolDefinition; }) { - const safeAddress = - this.#accountStateByAddress.get(signer.address)?.address ?? - computeProxyAddress(signer.address); + const accountState = await this.getAccountState({ + ownerAddress: signer.address, + }); + const isDepositWallet = accountState.walletType === 'deposit-wallet'; + const tradingWalletAddress = accountState.address; + const verifyingContract = getProtocolVerifyingContract({ + protocol, + negRisk: preview.negRisk, + }); + + let depositWalletSetupUpdatedState = false; + if (isDepositWallet) { + depositWalletSetupUpdatedState = await this.ensureDepositWalletReady({ + ownerAddress: signer.address, + depositWalletAddress: tradingWalletAddress, + protocol, + getSigner: () => signer, + operation: 'deposit_wallet_order_preflight', + }); + } const order = buildProtocolUnsignedOrder({ protocol, @@ -432,24 +506,22 @@ export class PolymarketProvider implements PredictProvider { ...preview, feeRateBps: getPreviewFeeRateBpsForProtocol(), }, - makerAddress: safeAddress, - signerAddress: getAddress(signer.address), + makerAddress: tradingWalletAddress, + signerAddress: isDepositWallet + ? tradingWalletAddress + : getAddress(signer.address), + signatureType: isDepositWallet + ? SignatureType.POLY_1271 + : SignatureType.POLY_GNOSIS_SAFE, }); - const typedData = getProtocolOrderTypedData({ + const signature = await signProtocolOrder({ + signer, protocol, order, - verifyingContract: getProtocolVerifyingContract({ - protocol, - negRisk: preview.negRisk, - }), + verifyingContract, }); - const signature = await signer.signTypedMessage( - { data: typedData, from: signer.address }, - SignTypedDataVersion.V4, - ); - const signedOrder = { ...order, signature, @@ -457,11 +529,22 @@ export class PolymarketProvider implements PredictProvider { const signerApiKey = await this.getApiKey({ address: signer.address, }); + + if (isDepositWallet && depositWalletSetupUpdatedState) { + await this.syncDepositWalletBalanceAllowanceForOrderIfNeeded({ + protocol, + signerAddress: signer.address, + apiKey: signerApiKey, + }); + } + const { feeCollection, fakOrdersEnabled } = this.#getFeatureFlags(); - const shouldUsePermit2 = this.#hasPermit2Config({ - permit2Enabled: preview.fees?.permit2Enabled, - executors: preview.fees?.executors, - }); + const shouldUsePermit2 = + !isDepositWallet && + this.#hasPermit2Config({ + permit2Enabled: preview.fees?.permit2Enabled, + executors: preview.fees?.executors, + }); let feeAuthorization: Permit2FeeAuthorization | undefined; let executor: string | undefined; @@ -477,7 +560,7 @@ export class PolymarketProvider implements PredictProvider { ); executor = this.#pickExecutor(preview.fees.executors ?? []); feeAuthorization = await createPermit2FeeAuthorization({ - safeAddress, + safeAddress: tradingWalletAddress, signer, amount: feeAmount, spender: executor, @@ -489,32 +572,53 @@ export class PolymarketProvider implements PredictProvider { let allowancesTx: { to: string; data: string } | undefined; let permit2AllowanceReady = false; - try { - const safeLegacyUsdceBalance = await this.#getLegacyUsdceBalance({ - safeAddress, - protocol, - }); - allowancesTx = await buildTradeAllowancesTx({ - signer, - safeAddress, - protocol, - safeUsdceBalance: safeLegacyUsdceBalance, - }); - permit2AllowanceReady = true; - } catch (allowanceError) { - DevLogger.log( - 'PolymarketProvider: Failed to generate v2 allowances transaction', - { error: allowanceError }, + if (isDepositWallet) { + const legacySafeAddress = computeProxyAddress(signer.address); + const legacySafeDeployed = await isSmartContractAddress( + legacySafeAddress, + numberToHex(POLYGON_MAINNET_CHAIN_ID), ); - Logger.error( - allowanceError instanceof Error - ? allowanceError - : new Error(String(allowanceError)), - this.getErrorContext('placeOrder:v2AllowancesTx', { - operation: 'generate_allowances_tx_v2', - }), - ); - throw new Error('Failed to prepare v2 trade preflight'); + + if (legacySafeDeployed) { + const sweepTransaction = await buildLegacySafeMigrationSweepTransaction( + { + signer, + legacySafeAddress, + depositWalletAddress: tradingWalletAddress, + protocol, + }, + ); + + allowancesTx = sweepTransaction?.params; + } + } else { + try { + const safeLegacyUsdceBalance = await this.#getLegacyUsdceBalance({ + safeAddress: tradingWalletAddress, + protocol, + }); + allowancesTx = await buildTradeAllowancesTx({ + signer, + safeAddress: tradingWalletAddress, + protocol, + safeUsdceBalance: safeLegacyUsdceBalance, + }); + permit2AllowanceReady = true; + } catch (allowanceError) { + DevLogger.log( + 'PolymarketProvider: Failed to generate v2 allowances transaction', + { error: allowanceError }, + ); + Logger.error( + allowanceError instanceof Error + ? allowanceError + : new Error(String(allowanceError)), + this.getErrorContext('placeOrder:v2AllowancesTx', { + operation: 'generate_allowances_tx_v2', + }), + ); + throw new Error('Failed to prepare v2 trade preflight'); + } } const orderType = this.#getPlaceOrderType({ @@ -523,6 +627,7 @@ export class PolymarketProvider implements PredictProvider { fakOrdersEnabled, permit2FeeReady, permit2AllowanceReady, + manualFeeCollectionSupported: !isDepositWallet, }); const clobOrder = serializeProtocolRelayerOrder({ @@ -538,7 +643,7 @@ export class PolymarketProvider implements PredictProvider { requestPath: `/order`, body, }, - address: clobOrder.order.signer ?? '', + address: signer.address, apiKey: signerApiKey, }); @@ -1436,7 +1541,9 @@ export class PolymarketProvider implements PredictProvider { throw new Error('Address is required'); } - const predictAddress = computeProxyAddress(address); + const predictAddress = + this.#getCachedAccountState(address)?.address ?? + (await this.getAccountState({ ownerAddress: address })).address; const queryParams = new URLSearchParams({ limit: limit.toString(), @@ -1519,7 +1626,7 @@ export class PolymarketProvider implements PredictProvider { try { const predictAddress = - this.#accountStateByAddress.get(address)?.address ?? + this.#getCachedAccountState(address)?.address ?? (await this.getAccountState({ ownerAddress: address })).address; const queryParams = new URLSearchParams({ @@ -1565,7 +1672,7 @@ export class PolymarketProvider implements PredictProvider { const { DATA_API_ENDPOINT } = getPolymarketEndpoints(); const predictAddress = - this.#accountStateByAddress.get(address)?.address ?? + this.#getCachedAccountState(address)?.address ?? (await this.getAccountState({ ownerAddress: address })).address; const response = await fetch( @@ -1783,24 +1890,17 @@ export class PolymarketProvider implements PredictProvider { throw new Error('Signer address is required for claim'); } - // Get safe address from cache or fetch it - let safeAddress: string | undefined; + let safeAddress: Hex; try { - safeAddress = - this.#accountStateByAddress.get(signer.address)?.address ?? - computeProxyAddress(signer.address); + safeAddress = computeProxyAddress(signer.address); } catch (error) { throw new Error( - `Failed to retrieve account state: ${ + `Failed to compute safe address: ${ error instanceof Error ? error.message : 'Unknown error' }`, ); } - if (!safeAddress) { - throw new Error('Safe address not found for claim'); - } - const safeLegacyUsdceBalance = await this.#getLegacyUsdceBalance({ safeAddress, protocol, @@ -1903,7 +2003,7 @@ export class PolymarketProvider implements PredictProvider { throw new Error('Signer address is required for deposit preparation'); } - const depositTokenAddress = getProtocolDepositTokenAddress(protocol); + const depositTokenAddress = protocol.collateral.tradingToken; if (!depositTokenAddress) { throw new Error('Collateral contract address not configured'); @@ -1921,45 +2021,64 @@ export class PolymarketProvider implements PredictProvider { throw new Error('Account address not found in account state'); } - if (!accountState.isDeployed) { - const deployTransaction = await getDeployProxyWalletTransaction({ - signer, + const buildDepositTransferTransaction = (toAddress: string) => { + const depositTransactionCallData = generateTransferData('transfer', { + toAddress, + amount: '0x0', }); - if (!deployTransaction) { - throw new Error('Failed to get deploy proxy wallet transaction params'); + if (!depositTransactionCallData) { + throw new Error( + 'Failed to generate transfer data for deposit transaction', + ); } - if (!deployTransaction.params?.to || !deployTransaction.params?.data) { - throw new Error('Invalid deploy transaction: missing params'); + return { + params: { + to: depositTokenAddress as Hex, + data: depositTransactionCallData as Hex, + }, + type: TransactionType.predictDeposit, + }; + }; + + if (accountState.walletType === 'deposit-wallet') { + const legacySafeAddress = computeProxyAddress(signer.address); + const legacySafeDeployed = await isSmartContractAddress( + legacySafeAddress, + numberToHex(POLYGON_MAINNET_CHAIN_ID), + ); + + if (legacySafeDeployed) { + const sweepTransaction = await buildLegacySafeMigrationSweepTransaction( + { + signer, + legacySafeAddress, + depositWalletAddress: accountState.address, + protocol, + }, + ); + + if (sweepTransaction) { + transactions.push(sweepTransaction); + } } - transactions.push(deployTransaction); + transactions.push(buildDepositTransferTransaction(accountState.address)); - // Set user trait for Polymarket account creation via MetaMask - this.setPolymarketAccountCreatedTrait(); + return { + chainId: CHAIN_IDS.POLYGON, + transactions, + }; } - const depositTransactionCallData = generateTransferData('transfer', { - toAddress: accountState.address, - amount: '0x0', - }); - - if (!depositTransactionCallData) { + if (!accountState.isDeployed) { throw new Error( - 'Failed to generate transfer data for deposit transaction', + 'Legacy Safe account state must be deployed for deposits', ); } - const depositTransaction = { - params: { - to: depositTokenAddress as Hex, - data: depositTransactionCallData as Hex, - }, - type: TransactionType.predictDeposit, - }; - - transactions.push(depositTransaction); + transactions.push(buildDepositTransferTransaction(accountState.address)); const preExistingSafeUsdceBalance = await this.#getLegacyUsdceBalance({ safeAddress: accountState.address, @@ -1982,21 +2101,57 @@ export class PolymarketProvider implements PredictProvider { }; } + async #hasPolymarketActivity({ + address, + }: { + address: string; + }): Promise { + const { DATA_API_ENDPOINT } = getPolymarketEndpoints(); + const queryParams = new URLSearchParams({ + user: address, + limit: '1', + }); + const response = await fetch( + `${DATA_API_ENDPOINT}/activity?${queryParams.toString()}`, + ); + + if (!response.ok) { + throw new Error('Failed to fetch Polymarket activity'); + } + + const activityRaw: unknown = await response.json(); + + if (!Array.isArray(activityRaw)) { + throw new Error('Polymarket activity response must be an array'); + } + + return activityRaw.length > 0; + } + public async getAccountState( params: GetAccountStateParams, ): Promise { try { - const { ownerAddress } = params; + const { ownerAddress, forceRefresh } = params; if (!ownerAddress) { throw new Error('Owner address is required'); } - // Get or compute safe address - const cachedAddress = this.#accountStateByAddress.get(ownerAddress); - let address: string; + const normalizedOwnerAddress = getAddress(ownerAddress); + + if (!forceRefresh) { + const cachedAccountState = this.#getCachedAccountState( + normalizedOwnerAddress, + ); + if (cachedAccountState) { + return cachedAccountState; + } + } + + let legacySafeAddress: Hex; try { - address = cachedAddress?.address ?? computeProxyAddress(ownerAddress); + legacySafeAddress = computeProxyAddress(normalizedOwnerAddress); } catch (error) { throw new Error( `Failed to compute safe address: ${ @@ -2005,30 +2160,41 @@ export class PolymarketProvider implements PredictProvider { ); } - if (!address) { - throw new Error('Failed to get safe address'); - } + const legacySafeIsDeployed = await isSmartContractAddress( + legacySafeAddress, + numberToHex(POLYGON_MAINNET_CHAIN_ID), + ); - let isDeployed: boolean; - try { - isDeployed = await isSmartContractAddress( - address, - numberToHex(POLYGON_MAINNET_CHAIN_ID), - ); - } catch (error) { - throw new Error( - `Failed to check account state: ${ - error instanceof Error ? error.message : 'Unknown error' - }`, - ); + if (legacySafeIsDeployed) { + const hasActivity = await this.#hasPolymarketActivity({ + address: legacySafeAddress, + }); + + if (hasActivity) { + const accountState: AccountState = { + address: legacySafeAddress, + isDeployed: true, + walletType: 'safe', + }; + this.#setCachedAccountState(normalizedOwnerAddress, accountState); + return accountState; + } } - const accountState = { - address: address as `0x${string}`, - isDeployed, + const depositWalletAddress = deriveDepositWalletAddress( + normalizedOwnerAddress, + ); + const depositWalletIsDeployed = await isSmartContractAddress( + depositWalletAddress, + numberToHex(POLYGON_MAINNET_CHAIN_ID), + ); + const accountState: AccountState = { + address: depositWalletAddress, + isDeployed: depositWalletIsDeployed, + walletType: 'deposit-wallet', }; - this.#accountStateByAddress.set(ownerAddress, accountState); + this.#setCachedAccountState(normalizedOwnerAddress, accountState); return accountState; } catch (error) { @@ -2046,25 +2212,375 @@ export class PolymarketProvider implements PredictProvider { throw new Error('address is required'); } - const predictAddress = - this.#accountStateByAddress.get(address)?.address ?? - computeProxyAddress(address); const protocol = this.#getProtocol(); + const accountState = + this.#getCachedAccountState(address) ?? + (await this.getAccountState({ ownerAddress: address })); + + if (accountState.walletType === 'safe') { + const [pusdBalance, legacyUsdceBalance] = await Promise.all([ + getBalance({ + address: accountState.address, + tokenAddress: protocol.collateral.tradingToken, + }), + this.#getLegacyUsdceBalance({ + safeAddress: accountState.address, + protocol, + }), + ]); - const [pusdBalance, legacyUsdceBalance] = await Promise.all([ - getBalance({ - address: predictAddress, - tokenAddress: protocol.collateral.tradingToken, - }), - this.#getLegacyUsdceBalance({ - safeAddress: predictAddress, - protocol, - }), - ]); + return ( + pusdBalance + + Number(legacyUsdceBalance) / 10 ** COLLATERAL_TOKEN_DECIMALS + ); + } + + const depositPusdRaw = await getRawBalance({ + address: accountState.address, + tokenAddress: protocol.collateral.tradingToken, + }); + const legacySafeAddress = computeProxyAddress(address); + const legacySafeDeployed = await isSmartContractAddress( + legacySafeAddress, + numberToHex(POLYGON_MAINNET_CHAIN_ID), + ); + + let legacyPusdRaw = 0n; + let legacyUsdceRaw = 0n; + if (legacySafeDeployed) { + [legacyPusdRaw, legacyUsdceRaw] = await Promise.all([ + getRawBalance({ + address: legacySafeAddress, + tokenAddress: protocol.collateral.tradingToken, + }), + this.#getLegacyUsdceBalance({ + safeAddress: legacySafeAddress, + protocol, + }), + ]); + } return ( - pusdBalance + Number(legacyUsdceBalance) / 10 ** COLLATERAL_TOKEN_DECIMALS + Number(depositPusdRaw + legacyPusdRaw + legacyUsdceRaw) / + 10 ** COLLATERAL_TOKEN_DECIMALS + ); + } + + private getErc20TransferRecipient(data?: string): Hex | undefined { + if (!data) { + return undefined; + } + + try { + const [recipient] = ERC20_TRANSFER_INTERFACE.decodeFunctionData( + 'transfer', + data, + ); + return getAddress(String(recipient)) as Hex; + } catch { + return undefined; + } + } + + private getDepositWalletDepositTransaction(transactionMeta: TransactionMeta): + | { + ownerAddress: Hex; + depositWalletAddress: Hex; + } + | undefined { + const ownerAddress = transactionMeta.txParams.from; + + if (!ownerAddress) { + return undefined; + } + + const nestedTransactions = transactionMeta.nestedTransactions ?? []; + const predictDepositTransactions = nestedTransactions.filter( + (transaction) => + transaction.type === TransactionType.predictDeposit || + transaction.type === TransactionType.predictDepositAndOrder, + ); + + if (predictDepositTransactions.length !== 1) { + return undefined; + } + + const [depositTransaction] = predictDepositTransactions; + const protocol = this.#getProtocol(); + + try { + if ( + !depositTransaction.to || + getAddress(depositTransaction.to) !== + getAddress(protocol.collateral.tradingToken) + ) { + return undefined; + } + } catch { + return undefined; + } + + const recipient = this.getErc20TransferRecipient(depositTransaction.data); + + if (!recipient) { + return undefined; + } + + const depositWalletAddress = deriveDepositWalletAddress(ownerAddress); + + if (getAddress(recipient) !== getAddress(depositWalletAddress)) { + return undefined; + } + + return { + ownerAddress: getAddress(ownerAddress) as Hex, + depositWalletAddress, + }; + } + + public isDepositWalletDepositTransaction( + transactionMeta: TransactionMeta, + ): boolean { + return Boolean(this.getDepositWalletDepositTransaction(transactionMeta)); + } + + private async ensureDepositWalletReady({ + ownerAddress, + depositWalletAddress, + protocol, + getSigner, + operation = 'deposit_wallet_preflight', + }: { + ownerAddress: string; + depositWalletAddress: Hex; + protocol: PolymarketProtocolDefinition; + getSigner: (address?: string) => Signer; + operation?: string; + }): Promise { + let updatedState = false; + let depositWalletDeploymentConfirmed = false; + + DevLogger.log('PolymarketProvider: Deposit wallet preflight started', { + operation, + walletType: 'deposit-wallet', + from: ownerAddress, + depositWalletAddress, + }); + + const depositWalletIsDeployed = await isSmartContractAddress( + depositWalletAddress, + numberToHex(POLYGON_MAINNET_CHAIN_ID), ); + + if (!depositWalletIsDeployed) { + const createResponse = await requestDepositWalletCreate({ + ownerAddress, + }); + const transactionID = + getDepositWalletRelayerTransactionId(createResponse); + + if (!transactionID) { + throw new Error( + 'Polymarket deposit wallet creation response missing transactionID', + ); + } + + DevLogger.log('PolymarketProvider: Waiting for deposit wallet create', { + operation: 'deposit_wallet_create', + walletType: 'deposit-wallet', + transactionID, + from: ownerAddress, + depositWalletAddress, + }); + + await waitForDepositWalletTransaction({ + transactionID, + requireCompletion: true, + }); + + DevLogger.log( + 'PolymarketProvider: Waiting for deposit wallet relayer registry', + { + operation: 'deposit_wallet_relayer_registry', + walletType: 'deposit-wallet', + from: ownerAddress, + depositWalletAddress, + }, + ); + + await waitForDepositWalletDeployed({ + walletAddress: depositWalletAddress, + }); + depositWalletDeploymentConfirmed = true; + + this.#setCachedAccountState(ownerAddress, { + address: depositWalletAddress, + isDeployed: true, + walletType: 'deposit-wallet', + }); + this.setPolymarketAccountCreatedTrait(); + updatedState = true; + } + + const preflightPlan = await planDepositWalletPreflight({ + walletAddress: depositWalletAddress, + protocol, + }); + + DevLogger.log('PolymarketProvider: Deposit wallet preflight planned', { + operation: 'deposit_wallet_allowance_preflight', + walletType: 'deposit-wallet', + from: ownerAddress, + depositWalletAddress, + missingRequirementsCount: preflightPlan.missingRequirements.length, + }); + + if (preflightPlan.transactions.length > 0) { + if (!depositWalletDeploymentConfirmed) { + await waitForDepositWalletDeployed({ + walletAddress: depositWalletAddress, + }); + } + + const signer = getSigner(ownerAddress); + const executeResponse = await executeDepositWalletBatch({ + signer, + walletAddress: depositWalletAddress, + calls: toDepositWalletCalls(preflightPlan.transactions), + }); + const transactionID = + getDepositWalletRelayerTransactionId(executeResponse); + + if (!transactionID) { + throw new Error( + 'Polymarket deposit wallet batch response missing transactionID', + ); + } + + DevLogger.log('PolymarketProvider: Waiting for deposit wallet batch', { + operation: 'deposit_wallet_batch', + walletType: 'deposit-wallet', + transactionID, + from: ownerAddress, + depositWalletAddress, + missingRequirementsCount: preflightPlan.missingRequirements.length, + }); + + await waitForDepositWalletTransaction({ + transactionID, + requireCompletion: true, + }); + updatedState = true; + } + + DevLogger.log('PolymarketProvider: Deposit wallet preflight completed', { + operation, + walletType: 'deposit-wallet', + from: ownerAddress, + depositWalletAddress, + }); + + return updatedState; + } + + public async beforePublishDepositWalletDeposit({ + transactionMeta, + getSigner, + }: { + transactionMeta: TransactionMeta; + getSigner: (address?: string) => Signer; + }): Promise { + const depositWalletDeposit = + this.getDepositWalletDepositTransaction(transactionMeta); + + if (!depositWalletDeposit) { + return true; + } + + const { ownerAddress, depositWalletAddress } = depositWalletDeposit; + const protocol = this.#getProtocol(); + + try { + await this.ensureDepositWalletReady({ + ownerAddress, + depositWalletAddress, + protocol, + getSigner, + }); + + return true; + } catch (error) { + DevLogger.log('PolymarketProvider: Deposit wallet preflight failed', { + operation: 'deposit_wallet_preflight', + walletType: 'deposit-wallet', + from: ownerAddress, + depositWalletAddress, + error: error instanceof Error ? error.message : 'Unknown error', + }); + Logger.error( + error instanceof Error ? error : new Error(String(error)), + this.getErrorContext('beforePublishDepositWalletDeposit', { + operation: 'deposit_wallet_preflight', + walletType: 'deposit-wallet', + }), + ); + throw error; + } + } + + private async syncDepositWalletBalanceAllowanceForOrderIfNeeded({ + protocol, + signerAddress, + apiKey, + }: { + protocol: PolymarketProtocolDefinition; + signerAddress: string; + apiKey: ApiKeyCreds; + }): Promise { + try { + await syncDepositWalletCollateralBalanceAllowance({ + protocol, + signerAddress, + apiKey, + }); + } catch (error) { + DevLogger.log( + 'PolymarketProvider: Deposit wallet order balance-allowance sync failed', + { + operation: 'deposit_wallet_order_balance_allowance_sync', + error: error instanceof Error ? error.message : 'Unknown error', + }, + ); + Logger.error( + error instanceof Error ? error : new Error(String(error)), + this.getErrorContext('placeOrder:depositWalletBalanceAllowanceSync', { + operation: 'deposit_wallet_order_balance_allowance_sync', + walletType: 'deposit-wallet', + }), + ); + } + } + + public async syncDepositWalletBalanceAllowanceForDepositTransaction({ + transactionMeta, + signerAddress, + }: { + transactionMeta: TransactionMeta; + signerAddress: string; + }): Promise { + const depositWalletDeposit = + this.getDepositWalletDepositTransaction(transactionMeta); + + if (!depositWalletDeposit) { + return; + } + + const apiKey = await this.getApiKey({ address: signerAddress }); + await syncDepositWalletCollateralBalanceAllowance({ + protocol: this.#getProtocol(), + signerAddress, + apiKey, + }); } public async prepareWithdraw( @@ -2078,7 +2594,7 @@ export class PolymarketProvider implements PredictProvider { } const safeAddress = - this.#accountStateByAddress.get(signer.address)?.address ?? + this.#getCachedAccountState(signer.address)?.address ?? (await this.getAccountState({ ownerAddress: signer.address })).address; const withdrawTokenAddress = getProtocolWithdrawTokenAddress(protocol); @@ -2113,7 +2629,7 @@ export class PolymarketProvider implements PredictProvider { } const safeAddress = - this.#accountStateByAddress.get(signer.address)?.address ?? + this.#getCachedAccountState(signer.address)?.address ?? computeProxyAddress(signer.address); const amount = getSafeTransferAmount(callData); diff --git a/app/components/UI/Predict/providers/polymarket/depositWallet.test.ts b/app/components/UI/Predict/providers/polymarket/depositWallet.test.ts new file mode 100644 index 00000000000..a0b79fd5a42 --- /dev/null +++ b/app/components/UI/Predict/providers/polymarket/depositWallet.test.ts @@ -0,0 +1,432 @@ +import { SignTypedDataVersion } from '@metamask/keyring-controller'; +import type { Signer } from '../types'; +import { + DEPOSIT_WALLET_FACTORY_ADDRESS, + deriveDepositWalletAddress, + executeDepositWalletBatch, + executeDepositWalletBatchAndWaitForCompletion, + getDepositWalletNonce, + requestDepositWalletCreate, + syncDepositWalletCollateralBalanceAllowance, + toDepositWalletCalls, + waitForDepositWalletDeployed, + waitForDepositWalletTransaction, +} from './depositWallet'; +import { POLYMARKET_V2_PROTOCOL } from './protocol/definitions'; +import { OperationType } from './safe/types'; +import { getL2Headers } from './utils'; + +jest.mock('./utils', () => ({ + getPolymarketEndpoints: jest.fn(() => ({ + CLOB_RELAYER: 'https://predict.api.cx.metamask.io', + })), + getL2Headers: jest.fn(), +})); + +const mockGetL2Headers = jest.mocked(getL2Headers); +const mockFetch = jest.fn(); + +type MockResponseBody = Record | Record[]; + +function mockResponse(body: MockResponseBody, ok = true): Response { + return { + ok, + status: ok ? 200 : 500, + text: jest.fn().mockResolvedValue(JSON.stringify(body)), + } as unknown as Response; +} + +function getFetchBody(callIndex = 0): unknown { + return JSON.parse(String(mockFetch.mock.calls[callIndex][1]?.body)); +} + +const ownerAddress = '0x1111111111111111111111111111111111111111'; + +const signer: Signer = { + address: ownerAddress, + signTypedMessage: jest.fn().mockResolvedValue('0xsignature'), + signPersonalMessage: jest.fn(), +}; + +describe('depositWallet', () => { + beforeEach(() => { + jest.clearAllMocks(); + global.fetch = mockFetch as unknown as typeof fetch; + mockGetL2Headers.mockResolvedValue({ + POLY_ADDRESS: ownerAddress, + POLY_SIGNATURE: 'signature', + POLY_TIMESTAMP: '1', + POLY_API_KEY: 'api-key', + POLY_PASSPHRASE: 'passphrase', + }); + }); + + it('derives the same address for checksum-equivalent owners', () => { + const lowerCaseAddress = ownerAddress.toLowerCase(); + const checkSummedAddress = ownerAddress.toUpperCase().replace('0X', '0x'); + + const lowerResult = deriveDepositWalletAddress(lowerCaseAddress); + const checksumResult = deriveDepositWalletAddress(checkSummedAddress); + + expect(lowerResult).toMatch(/^0x[a-fA-F0-9]{40}$/u); + expect(checksumResult).toBe(lowerResult); + }); + + it('maps Safe transactions to deposit-wallet calls', () => { + expect( + toDepositWalletCalls([ + { + to: POLYMARKET_V2_PROTOCOL.collateral.tradingToken, + value: '0', + data: '0x1234', + operation: OperationType.Call, + }, + ]), + ).toEqual([ + { + target: POLYMARKET_V2_PROTOCOL.collateral.tradingToken, + value: '0', + data: '0x1234', + }, + ]); + }); + + it('requests wallet creation through the relayer proxy envelope', async () => { + mockFetch.mockResolvedValue(mockResponse({ transactionID: 'relayer-1' })); + + const result = await requestDepositWalletCreate({ ownerAddress }); + + expect(result.transactionID).toBe('relayer-1'); + expect(mockFetch).toHaveBeenCalledWith( + 'https://predict.api.cx.metamask.io/transaction', + expect.objectContaining({ method: 'POST' }), + ); + expect(getFetchBody()).toEqual({ + path: '/submit', + method: 'POST', + body: { + type: 'WALLET-CREATE', + from: ownerAddress, + to: DEPOSIT_WALLET_FACTORY_ADDRESS, + }, + }); + }); + + it('reads wallet nonce through the relayer proxy envelope', async () => { + mockFetch.mockResolvedValue(mockResponse({ nonce: '7' })); + + const nonce = await getDepositWalletNonce({ ownerAddress }); + + expect(nonce).toBe('7'); + expect(getFetchBody()).toEqual({ + path: '/nonce', + method: 'GET', + query: { + address: ownerAddress, + type: 'WALLET', + }, + }); + }); + + it('signs and submits wallet batches through the relayer proxy envelope', async () => { + const walletAddress = deriveDepositWalletAddress(ownerAddress); + const calls = [ + { + target: POLYMARKET_V2_PROTOCOL.collateral.tradingToken, + value: '0', + data: '0x1234', + }, + ]; + mockFetch + .mockResolvedValueOnce(mockResponse({ nonce: '9' })) + .mockResolvedValueOnce(mockResponse({ transactionID: 'relayer-2' })); + + const result = await executeDepositWalletBatch({ + signer, + walletAddress, + calls, + }); + + expect(result.transactionID).toBe('relayer-2'); + expect(signer.signTypedMessage).toHaveBeenCalledWith( + expect.objectContaining({ + from: ownerAddress, + data: expect.objectContaining({ + primaryType: 'Batch', + message: expect.objectContaining({ + wallet: walletAddress, + nonce: '9', + calls, + }), + }), + }), + SignTypedDataVersion.V4, + ); + expect(getFetchBody(1)).toEqual({ + path: '/submit', + method: 'POST', + body: expect.objectContaining({ + type: 'WALLET', + from: ownerAddress, + to: DEPOSIT_WALLET_FACTORY_ADDRESS, + nonce: '9', + signature: '0xsignature', + depositWalletParams: expect.objectContaining({ + depositWallet: walletAddress, + calls, + }), + }), + }); + }); + + it('polls by relayer id until wallet transaction hash is available', async () => { + mockFetch.mockResolvedValue( + mockResponse({ + state: 'STATE_MINED', + transactionHash: + '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + }), + ); + + const hash = await waitForDepositWalletTransaction({ + transactionID: 'relayer-mined', + pollIntervalMs: 0, + }); + + expect(hash).toBe( + '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + ); + expect(mockFetch).toHaveBeenCalledTimes(1); + expect(getFetchBody()).toEqual({ + path: '/transaction', + method: 'GET', + query: { id: 'relayer-mined' }, + }); + }); + + it('returns hash before completion when completion is not required', async () => { + mockFetch.mockResolvedValue( + mockResponse({ + state: 'STATE_NEW', + transactionHash: + '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', + }), + ); + + const hash = await waitForDepositWalletTransaction({ + transactionID: 'relayer-hash', + pollIntervalMs: 0, + }); + + expect(hash).toBe( + '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', + ); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + + it('polls until wallet transaction is confirmed when completion is required', async () => { + mockFetch + .mockResolvedValueOnce( + mockResponse({ + state: 'STATE_NEW', + transactionHash: + '0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc', + }), + ) + .mockResolvedValueOnce( + mockResponse({ + state: 'STATE_CONFIRMED', + transactionHash: + '0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc', + }), + ); + + const hash = await waitForDepositWalletTransaction({ + transactionID: 'relayer-3', + requireCompletion: true, + pollIntervalMs: 0, + }); + + expect(hash).toBe( + '0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc', + ); + expect(mockFetch).toHaveBeenCalledTimes(2); + }); + + it.each(['STATE_FAILED', 'STATE_INVALID'])( + 'throws when wallet transaction polling reaches %s', + async (state) => { + mockFetch.mockResolvedValue(mockResponse({ state })); + + await expect( + waitForDepositWalletTransaction({ + transactionID: 'relayer-4', + pollIntervalMs: 0, + }), + ).rejects.toThrow(state); + }, + ); + + it('keeps polling when completion succeeds without a hash', async () => { + mockFetch + .mockResolvedValueOnce(mockResponse({ state: 'STATE_MINED' })) + .mockResolvedValueOnce( + mockResponse({ + state: 'STATE_MINED', + transactionHash: + '0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd', + }), + ); + + const hash = await waitForDepositWalletTransaction({ + transactionID: 'relayer-no-hash-yet', + requireCompletion: true, + pollIntervalMs: 0, + }); + + expect(hash).toBe( + '0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd', + ); + expect(mockFetch).toHaveBeenCalledTimes(2); + }); + + it('throws when wallet transaction polling times out', async () => { + mockFetch.mockResolvedValue(mockResponse({ state: 'STATE_NEW' })); + + await expect( + waitForDepositWalletTransaction({ + transactionID: 'relayer-timeout', + maxPolls: 2, + pollIntervalMs: 0, + }), + ).rejects.toThrow('Timed out'); + }); + + it('executes a wallet batch and waits for relayer completion', async () => { + const walletAddress = deriveDepositWalletAddress(ownerAddress); + const calls = [ + { + target: POLYMARKET_V2_PROTOCOL.collateral.tradingToken, + value: '0', + data: '0x1234', + }, + ]; + mockFetch + .mockResolvedValueOnce(mockResponse({ nonce: '11' })) + .mockResolvedValueOnce(mockResponse({ transactionID: 'relayer-5' })) + .mockResolvedValueOnce( + mockResponse({ + state: 'STATE_CONFIRMED', + transactionHash: + '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', + }), + ); + + const hash = await executeDepositWalletBatchAndWaitForCompletion({ + signer, + walletAddress, + calls, + }); + + expect(hash).toBe( + '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', + ); + expect(getFetchBody(2)).toEqual({ + path: '/transaction', + method: 'GET', + query: { id: 'relayer-5' }, + }); + }); + + it('throws when completed wallet batch response is missing transactionID', async () => { + const walletAddress = deriveDepositWalletAddress(ownerAddress); + const calls = [ + { + target: POLYMARKET_V2_PROTOCOL.collateral.tradingToken, + value: '0', + data: '0x1234', + }, + ]; + mockFetch + .mockResolvedValueOnce(mockResponse({ nonce: '12' })) + .mockResolvedValueOnce(mockResponse({})); + + await expect( + executeDepositWalletBatchAndWaitForCompletion({ + signer, + walletAddress, + calls, + }), + ).rejects.toThrow('missing transactionID'); + }); + + it('polls relayer deployment status through the MetaMask proxy until the wallet is registered', async () => { + const walletAddress = deriveDepositWalletAddress(ownerAddress); + mockFetch + .mockResolvedValueOnce(mockResponse({ deployed: false })) + .mockResolvedValueOnce(mockResponse({ deployed: true })); + + await waitForDepositWalletDeployed({ + walletAddress, + pollIntervalMs: 0, + }); + + expect(mockFetch).toHaveBeenCalledTimes(2); + expect(mockFetch).toHaveBeenCalledWith( + 'https://predict.api.cx.metamask.io/transaction', + expect.objectContaining({ method: 'POST' }), + ); + expect(getFetchBody()).toEqual({ + path: '/deployed', + method: 'GET', + query: { + address: walletAddress, + type: 'WALLET', + }, + }); + }); + + it('throws when relayer deployment status times out', async () => { + const walletAddress = deriveDepositWalletAddress(ownerAddress); + mockFetch.mockResolvedValue(mockResponse({ deployed: false })); + + await expect( + waitForDepositWalletDeployed({ + walletAddress, + maxPolls: 2, + pollIntervalMs: 0, + }), + ).rejects.toThrow('Timed out'); + }); + + it('syncs collateral balance allowance through direct CLOB endpoint', async () => { + mockFetch.mockResolvedValue(mockResponse({ ok: true })); + + await syncDepositWalletCollateralBalanceAllowance({ + protocol: POLYMARKET_V2_PROTOCOL, + signerAddress: ownerAddress, + apiKey: { + apiKey: 'api-key', + secret: 'secret', + passphrase: 'passphrase', + }, + }); + + expect(mockGetL2Headers).toHaveBeenCalledWith({ + l2HeaderArgs: { + method: 'GET', + requestPath: '/balance-allowance/update', + }, + address: ownerAddress, + apiKey: { + apiKey: 'api-key', + secret: 'secret', + passphrase: 'passphrase', + }, + }); + expect(mockFetch).toHaveBeenCalledWith( + 'https://clob.polymarket.com/balance-allowance/update?asset_type=COLLATERAL&signature_type=3', + expect.objectContaining({ method: 'GET' }), + ); + }); +}); diff --git a/app/components/UI/Predict/providers/polymarket/depositWallet.ts b/app/components/UI/Predict/providers/polymarket/depositWallet.ts new file mode 100644 index 00000000000..32e7d144900 --- /dev/null +++ b/app/components/UI/Predict/providers/polymarket/depositWallet.ts @@ -0,0 +1,450 @@ +import { SignTypedDataVersion } from '@metamask/keyring-controller'; +import { Hex } from '@metamask/utils'; +import { ethers } from 'ethers'; +import { + getAddress, + getCreate2Address, + hexConcat, + hexZeroPad, + keccak256, +} from 'ethers/lib/utils'; +import { POLYGON_MAINNET_CHAIN_ID } from './constants'; +import type { PolymarketProtocolDefinition } from './protocol/definitions'; +import type { SafeTransaction } from './safe/types'; +import type { ApiKeyCreds } from './types'; +import type { Signer } from '../types'; +import { getL2Headers, getPolymarketEndpoints } from './utils'; + +export const DEPOSIT_WALLET_FACTORY_ADDRESS = + '0x00000000000Fb5C9ADea0298D729A0CB3823Cc07'; +export const DEPOSIT_WALLET_IMPLEMENTATION_ADDRESS = + '0x58CA52ebe0DadfdF531Cde7062e76746de4Db1eB'; + +const DEPOSIT_WALLET_DOMAIN_NAME = 'DepositWallet'; +const DEPOSIT_WALLET_DOMAIN_VERSION = '1'; +// Polymarket's relayer rejects deadlines above 300s, so use the maximum +// allowed window to reduce intermittent "deadline too soon" failures. +const DEPOSIT_WALLET_BATCH_DEADLINE_SECONDS = 300; + +const RELAYER_SUCCESS_STATES = new Set(['STATE_MINED', 'STATE_CONFIRMED']); +const RELAYER_FAILURE_STATES = new Set(['STATE_FAILED', 'STATE_INVALID']); + +/** + * Byte constants from Solady LibClone.initCodeHashERC1967 used by the + * Polymarket DepositWalletFactory. + */ +const ERC1967_CONST1 = + '0xcc3735a920a3ca505d382bbc545af43d6000803e6038573d6000fd5b3d6000f3'; +const ERC1967_CONST2 = + '0x5155f3363d3d373d3d363d7f360894a13ba1a3210667c828492db98dca3e2076'; +const ERC1967_PREFIX = 0x61003d3d8160233d3973n; + +export interface DepositWalletCall { + target: string; + value: string; + data: string; +} + +export interface DepositWalletRelayerResponse { + transactionID?: string; + transactionHash?: string; + state?: string; + nonce?: string; + error?: string; + id?: string; + [key: string]: unknown; +} + +interface DepositWalletDeploymentResponse { + deployed?: boolean; + error?: string; + [key: string]: unknown; +} + +export type RelayerProxyEnvelope = + | { path: '/submit'; method: 'POST'; body: unknown } + | { path: '/nonce'; method: 'GET'; query: Record } + | { path: '/transaction'; method: 'GET'; query: Record } + | { path: '/deployed'; method: 'GET'; query: Record }; + +function toFixedSizeHex(value: bigint, sizeBytes: number): Hex { + const hex = value.toString(16).padStart(sizeBytes * 2, '0'); + + if (hex.length > sizeBytes * 2) { + throw new Error('Value exceeds requested byte size'); + } + + return `0x${hex}` as Hex; +} + +function initCodeHashERC1967({ + implementation, + args, +}: { + implementation: string; + args: Hex; +}): Hex { + const argsLength = BigInt((args.length - 2) / 2); + const prefix = toFixedSizeHex(ERC1967_PREFIX + (argsLength << 56n), 10); + + return keccak256( + hexConcat([ + prefix, + getAddress(implementation), + '0x6009', + ERC1967_CONST2, + ERC1967_CONST1, + args, + ]), + ) as Hex; +} + +export function getDepositWalletId(ownerAddress: string): Hex { + return hexZeroPad(getAddress(ownerAddress), 32) as Hex; +} + +export function deriveDepositWalletAddress(ownerAddress: string): Hex { + const factoryAddress = getAddress(DEPOSIT_WALLET_FACTORY_ADDRESS); + const implementationAddress = getAddress( + DEPOSIT_WALLET_IMPLEMENTATION_ADDRESS, + ); + const walletId = getDepositWalletId(ownerAddress); + const args = ethers.utils.defaultAbiCoder.encode( + ['address', 'bytes32'], + [factoryAddress, walletId], + ) as Hex; + const salt = keccak256(args) as Hex; + const bytecodeHash = initCodeHashERC1967({ + implementation: implementationAddress, + args, + }); + + return getAddress( + getCreate2Address(factoryAddress, salt, bytecodeHash), + ) as Hex; +} + +export function toDepositWalletCalls( + transactions: SafeTransaction[], +): DepositWalletCall[] { + return transactions.map((transaction) => ({ + target: transaction.to, + value: transaction.value, + data: transaction.data, + })); +} + +async function postRelayerProxy( + envelope: RelayerProxyEnvelope, +): Promise { + const { CLOB_RELAYER } = getPolymarketEndpoints(); + const response = await fetch(`${CLOB_RELAYER}/transaction`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(envelope), + }); + const responseText = await response.text(); + + if (!response.ok) { + throw new Error( + `Polymarket relayer proxy request failed: ${response.status} ${responseText}`, + ); + } + + if (!responseText) { + throw new Error('Polymarket relayer proxy returned an empty response'); + } + + let data: unknown; + try { + data = JSON.parse(responseText); + } catch (error) { + throw new Error('Polymarket relayer proxy returned malformed JSON'); + } + + if ( + typeof data === 'object' && + data !== null && + 'error' in data && + typeof (data as DepositWalletRelayerResponse).error === 'string' + ) { + throw new Error((data as DepositWalletRelayerResponse).error); + } + + return data as TResponse; +} + +export function getDepositWalletRelayerTransactionId( + response: DepositWalletRelayerResponse, +): string | undefined { + return response.transactionID ?? response.id; +} + +export async function requestDepositWalletCreate({ + ownerAddress, +}: { + ownerAddress: string; +}): Promise { + return postRelayerProxy({ + path: '/submit', + method: 'POST', + body: { + type: 'WALLET-CREATE', + from: ownerAddress, + to: DEPOSIT_WALLET_FACTORY_ADDRESS, + }, + }); +} + +export async function getDepositWalletNonce({ + ownerAddress, +}: { + ownerAddress: string; +}): Promise { + const response = await postRelayerProxy({ + path: '/nonce', + method: 'GET', + query: { + address: ownerAddress, + type: 'WALLET', + }, + }); + + if (!response.nonce) { + throw new Error('Polymarket relayer proxy nonce response missing nonce'); + } + + return response.nonce; +} + +const DEPOSIT_WALLET_EIP712_TYPES = { + EIP712Domain: [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' }, + { name: 'chainId', type: 'uint256' }, + { name: 'verifyingContract', type: 'address' }, + ], + Call: [ + { name: 'target', type: 'address' }, + { name: 'value', type: 'uint256' }, + { name: 'data', type: 'bytes' }, + ], + Batch: [ + { name: 'wallet', type: 'address' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, + { name: 'calls', type: 'Call[]' }, + ], +}; + +export async function executeDepositWalletBatch({ + signer, + walletAddress, + calls, +}: { + signer: Signer; + walletAddress: string; + calls: DepositWalletCall[]; +}): Promise { + if (calls.length === 0) { + throw new Error('Deposit wallet batch requires at least one call'); + } + + const nonce = await getDepositWalletNonce({ ownerAddress: signer.address }); + const deadline = String( + Math.floor(Date.now() / 1000) + DEPOSIT_WALLET_BATCH_DEADLINE_SECONDS, + ); + const normalizedWalletAddress = getAddress(walletAddress); + + const signature = await signer.signTypedMessage( + { + from: signer.address, + data: { + domain: { + name: DEPOSIT_WALLET_DOMAIN_NAME, + version: DEPOSIT_WALLET_DOMAIN_VERSION, + chainId: POLYGON_MAINNET_CHAIN_ID, + verifyingContract: normalizedWalletAddress, + }, + types: DEPOSIT_WALLET_EIP712_TYPES, + primaryType: 'Batch', + message: { + wallet: normalizedWalletAddress, + nonce, + deadline, + calls, + }, + }, + }, + SignTypedDataVersion.V4, + ); + + return postRelayerProxy({ + path: '/submit', + method: 'POST', + body: { + type: 'WALLET', + from: signer.address, + to: DEPOSIT_WALLET_FACTORY_ADDRESS, + nonce, + signature, + depositWalletParams: { + depositWallet: normalizedWalletAddress, + deadline, + calls, + }, + }, + }); +} + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +function getTransactionResponse( + response: DepositWalletRelayerResponse | DepositWalletRelayerResponse[], +): DepositWalletRelayerResponse | undefined { + return Array.isArray(response) ? response[0] : response; +} + +export async function waitForDepositWalletTransaction({ + transactionID, + requireCompletion = false, + maxPolls = 20, + pollIntervalMs = 1000, +}: { + transactionID: string; + requireCompletion?: boolean; + maxPolls?: number; + pollIntervalMs?: number; +}): Promise { + for (let poll = 0; poll < maxPolls; poll++) { + const response = await postRelayerProxy< + DepositWalletRelayerResponse | DepositWalletRelayerResponse[] + >({ + path: '/transaction', + method: 'GET', + query: { id: transactionID }, + }); + const transaction = getTransactionResponse(response); + const state = transaction?.state; + const transactionHash = transaction?.transactionHash; + + if (state && RELAYER_FAILURE_STATES.has(state)) { + throw new Error( + `Polymarket deposit wallet relayer transaction ${transactionID} ${state}`, + ); + } + + if (state && RELAYER_SUCCESS_STATES.has(state) && transactionHash) { + return transactionHash as Hex; + } + + if (!requireCompletion && transactionHash) { + return transactionHash as Hex; + } + + if (poll < maxPolls - 1) { + await sleep(pollIntervalMs); + } + } + + throw new Error( + `Timed out waiting for Polymarket deposit wallet relayer transaction ${transactionID}`, + ); +} + +export async function executeDepositWalletBatchAndWaitForCompletion({ + signer, + walletAddress, + calls, +}: { + signer: Signer; + walletAddress: string; + calls: DepositWalletCall[]; +}): Promise { + const response = await executeDepositWalletBatch({ + signer, + walletAddress, + calls, + }); + const transactionID = getDepositWalletRelayerTransactionId(response); + + if (!transactionID) { + throw new Error( + 'Polymarket deposit wallet batch response missing transactionID', + ); + } + + return waitForDepositWalletTransaction({ + transactionID, + requireCompletion: true, + }); +} + +export async function waitForDepositWalletDeployed({ + walletAddress, + maxPolls = 20, + pollIntervalMs = 1000, +}: { + walletAddress: string; + maxPolls?: number; + pollIntervalMs?: number; +}): Promise { + const normalizedWalletAddress = getAddress(walletAddress); + + for (let poll = 0; poll < maxPolls; poll++) { + const response = await postRelayerProxy({ + path: '/deployed', + method: 'GET', + query: { + address: normalizedWalletAddress, + type: 'WALLET', + }, + }); + + if (response.deployed === true) { + return; + } + + if (poll < maxPolls - 1) { + await sleep(pollIntervalMs); + } + } + + throw new Error( + `Timed out waiting for Polymarket deposit wallet ${normalizedWalletAddress} to be recognized by relayer`, + ); +} + +export async function syncDepositWalletCollateralBalanceAllowance({ + protocol, + signerAddress, + apiKey, +}: { + protocol: PolymarketProtocolDefinition; + signerAddress: string; + apiKey: ApiKeyCreds; +}): Promise { + const requestPath = '/balance-allowance/update'; + const queryString = 'asset_type=COLLATERAL&signature_type=3'; + const headers = await getL2Headers({ + l2HeaderArgs: { method: 'GET', requestPath }, + address: signerAddress, + apiKey, + }); + const response = await fetch( + `${protocol.transport.clobBaseUrl}${requestPath}?${queryString}`, + { + method: 'GET', + headers, + }, + ); + + if (!response.ok) { + const responseText = await response.text(); + throw new Error( + `Failed to sync deposit wallet collateral balance allowance: ${response.status} ${responseText}`, + ); + } +} diff --git a/app/components/UI/Predict/providers/polymarket/preflight/depositWallet.test.ts b/app/components/UI/Predict/providers/polymarket/preflight/depositWallet.test.ts new file mode 100644 index 00000000000..82faa08d4b7 --- /dev/null +++ b/app/components/UI/Predict/providers/polymarket/preflight/depositWallet.test.ts @@ -0,0 +1,107 @@ +jest.mock('./v2AllowanceRequirements', () => ({ + filterDepositWalletUnsupportedRequirements: jest.fn( + (requirements: { type: string; spender?: string }[]) => + requirements.filter( + (requirement: { type: string; spender?: string }) => + requirement.type !== 'erc20-allowance' || + requirement.spender?.toLowerCase() !== + '0x000000000022d473030f116ddee9f6b43ac78ba3', + ), + ), + getActiveV2AllowanceRequirements: jest.fn(), +})); + +jest.mock('./inspectMissingRequirements', () => ({ + inspectMissingRequirements: jest.fn(), +})); + +jest.mock('./compileRequirementTransactions', () => ({ + compileRequirementTransactions: jest.fn(), +})); + +import { PERMIT2_ADDRESS } from '../safe/constants'; +import { OperationType, type SafeTransaction } from '../safe/types'; +import { compileRequirementTransactions } from './compileRequirementTransactions'; +import { planDepositWalletPreflight } from './depositWallet'; +import { inspectMissingRequirements } from './inspectMissingRequirements'; +import { + filterDepositWalletUnsupportedRequirements, + getActiveV2AllowanceRequirements, + type V2AllowanceRequirement, +} from './v2AllowanceRequirements'; + +const walletAddress = '0x1111111111111111111111111111111111111111'; +const activeRequirement: V2AllowanceRequirement = { + type: 'erc20-allowance', + tokenAddress: '0x2222222222222222222222222222222222222222', + spender: '0x3333333333333333333333333333333333333333', +}; +const permit2Requirement: V2AllowanceRequirement = { + type: 'erc20-allowance', + tokenAddress: '0x2222222222222222222222222222222222222222', + spender: PERMIT2_ADDRESS, +}; +const activeRequirements: V2AllowanceRequirement[] = [ + activeRequirement, + permit2Requirement, +]; +const depositWalletRequirements: V2AllowanceRequirement[] = [activeRequirement]; +const missingRequirements: V2AllowanceRequirement[] = [activeRequirement]; +const compiledTransactions: SafeTransaction[] = [ + { + to: '0x2222222222222222222222222222222222222222', + data: '0xapprove', + operation: OperationType.Call, + value: '0', + }, +]; + +const mockFilterDepositWalletUnsupportedRequirements = jest.mocked( + filterDepositWalletUnsupportedRequirements, +); +const mockGetActiveV2AllowanceRequirements = jest.mocked( + getActiveV2AllowanceRequirements, +); +const mockInspectMissingRequirements = jest.mocked(inspectMissingRequirements); +const mockCompileRequirementTransactions = jest.mocked( + compileRequirementTransactions, +); + +describe('planDepositWalletPreflight', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockGetActiveV2AllowanceRequirements.mockReturnValue(activeRequirements); + mockInspectMissingRequirements.mockResolvedValue(missingRequirements); + mockCompileRequirementTransactions.mockReturnValue(compiledTransactions); + }); + + it('uses active V2 requirements without Permit2 for the deposit wallet and compiles missing repairs', async () => { + const plan = await planDepositWalletPreflight({ walletAddress }); + + expect(mockGetActiveV2AllowanceRequirements).toHaveBeenCalledTimes(1); + expect(mockFilterDepositWalletUnsupportedRequirements).toHaveBeenCalledWith( + activeRequirements, + ); + expect(mockInspectMissingRequirements).toHaveBeenCalledWith({ + address: walletAddress, + requirements: depositWalletRequirements, + }); + expect(mockCompileRequirementTransactions).toHaveBeenCalledWith( + missingRequirements, + ); + expect(plan).toEqual({ + missingRequirements, + transactions: compiledTransactions, + }); + }); + + it('returns no transactions when no requirements are missing', async () => { + mockInspectMissingRequirements.mockResolvedValue([]); + mockCompileRequirementTransactions.mockReturnValue([]); + + const plan = await planDepositWalletPreflight({ walletAddress }); + + expect(mockCompileRequirementTransactions).toHaveBeenCalledWith([]); + expect(plan).toEqual({ missingRequirements: [], transactions: [] }); + }); +}); diff --git a/app/components/UI/Predict/providers/polymarket/preflight/depositWallet.ts b/app/components/UI/Predict/providers/polymarket/preflight/depositWallet.ts new file mode 100644 index 00000000000..08afd79f031 --- /dev/null +++ b/app/components/UI/Predict/providers/polymarket/preflight/depositWallet.ts @@ -0,0 +1,44 @@ +import { + POLYMARKET_V2_PROTOCOL, + type PolymarketProtocolDefinition, +} from '../protocol/definitions'; +import type { SafeTransaction } from '../safe/types'; +import { compileRequirementTransactions } from './compileRequirementTransactions'; +import { inspectMissingRequirements } from './inspectMissingRequirements'; +import { + filterDepositWalletUnsupportedRequirements, + getActiveV2AllowanceRequirements, + type V2AllowanceRequirement, +} from './v2AllowanceRequirements'; + +export interface DepositWalletPreflightPlan { + missingRequirements: V2AllowanceRequirement[]; + transactions: SafeTransaction[]; +} + +function getDepositWalletAllowanceRequirements( + protocol: PolymarketProtocolDefinition, +): V2AllowanceRequirement[] { + return filterDepositWalletUnsupportedRequirements( + getActiveV2AllowanceRequirements(protocol), + ); +} + +export async function planDepositWalletPreflight({ + walletAddress, + protocol = POLYMARKET_V2_PROTOCOL, +}: { + walletAddress: string; + protocol?: PolymarketProtocolDefinition; +}): Promise { + const requirements = getDepositWalletAllowanceRequirements(protocol); + const missingRequirements = await inspectMissingRequirements({ + address: walletAddress, + requirements, + }); + + return { + missingRequirements, + transactions: compileRequirementTransactions(missingRequirements), + }; +} diff --git a/app/components/UI/Predict/providers/polymarket/preflight/legacySafeMigration.test.ts b/app/components/UI/Predict/providers/polymarket/preflight/legacySafeMigration.test.ts new file mode 100644 index 00000000000..aebb781eb09 --- /dev/null +++ b/app/components/UI/Predict/providers/polymarket/preflight/legacySafeMigration.test.ts @@ -0,0 +1,234 @@ +import { TransactionType } from '@metamask/transaction-controller'; + +jest.mock('./core', () => { + const actual = jest.requireActual('./core'); + + return { + ...actual, + buildSignedSafeExecutionIfNeeded: jest.fn(), + getRawTokenBalance: jest.fn(), + }; +}); + +jest.mock('../utils', () => ({ + encodeApprove: jest.fn(() => '0xapprove'), + encodeErc20Transfer: jest.fn(() => '0xtransfer'), + getAllowance: jest.fn(), +})); + +jest.mock('../protocol/orderCodec', () => ({ + encodeWrap: jest.fn(() => '0xwrap'), +})); + +import type { Hex } from '@metamask/utils'; +import type { Signer } from '../../types'; +import { POLYMARKET_V2_PROTOCOL } from '../protocol/definitions'; +import { encodeWrap } from '../protocol/orderCodec'; +import { OperationType } from '../safe/types'; +import { encodeApprove, encodeErc20Transfer, getAllowance } from '../utils'; +import { + buildSignedSafeExecutionIfNeeded, + getRawTokenBalance, + type SignedSafeExecution, +} from './core'; +import { + buildLegacySafeMigrationSweepTransaction, + planLegacySafeMigrationSweep, +} from './legacySafeMigration'; + +const legacySafeAddress = '0x1111111111111111111111111111111111111111'; +const depositWalletAddress = '0x2222222222222222222222222222222222222222'; +const signer: Signer = { + address: '0x3333333333333333333333333333333333333333', + signPersonalMessage: jest.fn(), + signTypedMessage: jest.fn(), +}; + +const mockGetRawTokenBalance = jest.mocked(getRawTokenBalance); +const mockGetAllowance = jest.mocked(getAllowance); +const mockEncodeApprove = jest.mocked(encodeApprove); +const mockEncodeWrap = jest.mocked(encodeWrap); +const mockEncodeErc20Transfer = jest.mocked(encodeErc20Transfer); +const mockBuildSignedSafeExecutionIfNeeded = jest.mocked( + buildSignedSafeExecutionIfNeeded, +); + +function mockBalances({ usdce, pusd }: { usdce: bigint; pusd: bigint }): void { + mockGetRawTokenBalance + .mockResolvedValueOnce(usdce) + .mockResolvedValueOnce(pusd); +} + +describe('legacy Safe migration sweep', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockGetAllowance.mockResolvedValue(0n); + }); + + it('returns no transactions when the legacy Safe has no sweepable balances', async () => { + mockBalances({ usdce: 0n, pusd: 0n }); + + const plan = await planLegacySafeMigrationSweep({ + legacySafeAddress, + depositWalletAddress, + protocol: POLYMARKET_V2_PROTOCOL, + }); + + expect(plan).toEqual({ + legacyUsdceBalance: 0n, + legacyPusdBalance: 0n, + transactions: [], + }); + expect(mockGetAllowance).not.toHaveBeenCalled(); + }); + + it('adds USDC.e approve then wrap when allowance is below balance', async () => { + mockBalances({ usdce: 1_000_000n, pusd: 0n }); + mockGetAllowance.mockResolvedValue(0n); + + const plan = await planLegacySafeMigrationSweep({ + legacySafeAddress, + depositWalletAddress, + protocol: POLYMARKET_V2_PROTOCOL, + }); + + expect(mockGetAllowance).toHaveBeenCalledWith({ + tokenAddress: POLYMARKET_V2_PROTOCOL.collateral.legacyUsdceToken, + owner: legacySafeAddress, + spender: POLYMARKET_V2_PROTOCOL.collateral.onrampAddress, + }); + expect(mockEncodeApprove).toHaveBeenCalledWith({ + spender: POLYMARKET_V2_PROTOCOL.collateral.onrampAddress, + amount: expect.anything(), + }); + expect(mockEncodeWrap).toHaveBeenCalledWith({ + asset: POLYMARKET_V2_PROTOCOL.collateral.legacyUsdceToken, + to: depositWalletAddress, + amount: 1_000_000n, + }); + expect(plan.transactions).toEqual([ + { + to: POLYMARKET_V2_PROTOCOL.collateral.legacyUsdceToken, + data: '0xapprove', + operation: OperationType.Call, + value: '0', + }, + { + to: POLYMARKET_V2_PROTOCOL.collateral.onrampAddress, + data: '0xwrap', + operation: OperationType.Call, + value: '0', + }, + ]); + }); + + it('skips USDC.e approve when allowance covers the balance', async () => { + mockBalances({ usdce: 1_000_000n, pusd: 0n }); + mockGetAllowance.mockResolvedValue(1_000_000n); + + const plan = await planLegacySafeMigrationSweep({ + legacySafeAddress, + depositWalletAddress, + protocol: POLYMARKET_V2_PROTOCOL, + }); + + expect(mockEncodeApprove).not.toHaveBeenCalled(); + expect(plan.transactions.map((transaction) => transaction.to)).toEqual([ + POLYMARKET_V2_PROTOCOL.collateral.onrampAddress, + ]); + }); + + it('adds pUSD transfer to the deposit wallet', async () => { + mockBalances({ usdce: 0n, pusd: 2_000_000n }); + + const plan = await planLegacySafeMigrationSweep({ + legacySafeAddress, + depositWalletAddress, + protocol: POLYMARKET_V2_PROTOCOL, + }); + + expect(mockEncodeErc20Transfer).toHaveBeenCalledWith({ + to: depositWalletAddress, + value: 2_000_000n, + }); + expect(plan.transactions).toEqual([ + { + to: POLYMARKET_V2_PROTOCOL.collateral.tradingToken, + data: '0xtransfer', + operation: OperationType.Call, + value: '0', + }, + ]); + }); + + it('orders both balances as USDC.e approve/wrap before pUSD transfer', async () => { + mockBalances({ usdce: 1_000_000n, pusd: 2_000_000n }); + mockGetAllowance.mockResolvedValue(0n); + + const plan = await planLegacySafeMigrationSweep({ + legacySafeAddress, + depositWalletAddress, + protocol: POLYMARKET_V2_PROTOCOL, + }); + + expect(plan.transactions.map((transaction) => transaction.to)).toEqual([ + POLYMARKET_V2_PROTOCOL.collateral.legacyUsdceToken, + POLYMARKET_V2_PROTOCOL.collateral.onrampAddress, + POLYMARKET_V2_PROTOCOL.collateral.tradingToken, + ]); + }); + + it('returns undefined from the signed sweep builder when there are no transactions', async () => { + mockBalances({ usdce: 0n, pusd: 0n }); + mockBuildSignedSafeExecutionIfNeeded.mockResolvedValue(undefined); + + const result = await buildLegacySafeMigrationSweepTransaction({ + signer, + legacySafeAddress, + depositWalletAddress, + protocol: POLYMARKET_V2_PROTOCOL, + }); + + expect(result).toBeUndefined(); + expect(mockBuildSignedSafeExecutionIfNeeded).toHaveBeenCalledWith({ + signer, + safeAddress: legacySafeAddress, + transactions: [], + type: TransactionType.contractInteraction, + }); + }); + + it('signs a Safe execution for sweep transactions', async () => { + mockBalances({ usdce: 0n, pusd: 2_000_000n }); + const signedSweep: SignedSafeExecution = { + params: { + to: legacySafeAddress as Hex, + data: '0xsigned' as Hex, + }, + type: TransactionType.contractInteraction, + }; + mockBuildSignedSafeExecutionIfNeeded.mockResolvedValue(signedSweep); + + const result = await buildLegacySafeMigrationSweepTransaction({ + signer, + legacySafeAddress, + depositWalletAddress, + protocol: POLYMARKET_V2_PROTOCOL, + }); + + expect(result).toBe(signedSweep); + expect(mockBuildSignedSafeExecutionIfNeeded).toHaveBeenCalledWith({ + signer, + safeAddress: legacySafeAddress, + transactions: [ + { + to: POLYMARKET_V2_PROTOCOL.collateral.tradingToken, + data: '0xtransfer', + operation: OperationType.Call, + value: '0', + }, + ], + type: TransactionType.contractInteraction, + }); + }); +}); diff --git a/app/components/UI/Predict/providers/polymarket/preflight/legacySafeMigration.ts b/app/components/UI/Predict/providers/polymarket/preflight/legacySafeMigration.ts new file mode 100644 index 00000000000..a422f8e8748 --- /dev/null +++ b/app/components/UI/Predict/providers/polymarket/preflight/legacySafeMigration.ts @@ -0,0 +1,117 @@ +import { TransactionType } from '@metamask/transaction-controller'; +import { ethers } from 'ethers'; +import type { Signer } from '../../types'; +import { + POLYMARKET_V2_PROTOCOL, + type PolymarketProtocolDefinition, +} from '../protocol/definitions'; +import { encodeWrap } from '../protocol/orderCodec'; +import { OperationType, type SafeTransaction } from '../safe/types'; +import { encodeApprove, encodeErc20Transfer, getAllowance } from '../utils'; +import { + buildSignedSafeExecutionIfNeeded, + getRawTokenBalance, + type SignedSafeExecution, +} from './core'; + +export interface LegacySafeMigrationSweepPlan { + legacyUsdceBalance: bigint; + legacyPusdBalance: bigint; + transactions: SafeTransaction[]; +} + +export async function planLegacySafeMigrationSweep({ + legacySafeAddress, + depositWalletAddress, + protocol = POLYMARKET_V2_PROTOCOL, +}: { + legacySafeAddress: string; + depositWalletAddress: string; + protocol?: PolymarketProtocolDefinition; +}): Promise { + const [legacyUsdceBalance, legacyPusdBalance] = await Promise.all([ + getRawTokenBalance({ + address: legacySafeAddress, + tokenAddress: protocol.collateral.legacyUsdceToken, + }), + getRawTokenBalance({ + address: legacySafeAddress, + tokenAddress: protocol.collateral.tradingToken, + }), + ]); + const transactions: SafeTransaction[] = []; + + if (legacyUsdceBalance > 0n) { + const allowance = await getAllowance({ + tokenAddress: protocol.collateral.legacyUsdceToken, + owner: legacySafeAddress, + spender: protocol.collateral.onrampAddress, + }); + + if (allowance < legacyUsdceBalance) { + transactions.push({ + to: protocol.collateral.legacyUsdceToken, + data: encodeApprove({ + spender: protocol.collateral.onrampAddress, + amount: ethers.constants.MaxUint256.toBigInt(), + }), + operation: OperationType.Call, + value: '0', + }); + } + + transactions.push({ + to: protocol.collateral.onrampAddress, + data: encodeWrap({ + asset: protocol.collateral.legacyUsdceToken, + to: depositWalletAddress, + amount: legacyUsdceBalance, + }), + operation: OperationType.Call, + value: '0', + }); + } + + if (legacyPusdBalance > 0n) { + transactions.push({ + to: protocol.collateral.tradingToken, + data: encodeErc20Transfer({ + to: depositWalletAddress, + value: legacyPusdBalance, + }), + operation: OperationType.Call, + value: '0', + }); + } + + return { + legacyUsdceBalance, + legacyPusdBalance, + transactions, + }; +} + +export async function buildLegacySafeMigrationSweepTransaction({ + signer, + legacySafeAddress, + depositWalletAddress, + protocol = POLYMARKET_V2_PROTOCOL, +}: { + signer: Signer; + legacySafeAddress: string; + depositWalletAddress: string; + protocol?: PolymarketProtocolDefinition; +}): Promise { + const plan = await planLegacySafeMigrationSweep({ + legacySafeAddress, + depositWalletAddress, + protocol, + }); + + return buildSignedSafeExecutionIfNeeded({ + signer, + safeAddress: legacySafeAddress, + transactions: plan.transactions, + type: TransactionType.contractInteraction, + }); +} diff --git a/app/components/UI/Predict/providers/polymarket/preflight/v2AllowanceRequirements.test.ts b/app/components/UI/Predict/providers/polymarket/preflight/v2AllowanceRequirements.test.ts index 2c6ff66433c..6abf83baaff 100644 --- a/app/components/UI/Predict/providers/polymarket/preflight/v2AllowanceRequirements.test.ts +++ b/app/components/UI/Predict/providers/polymarket/preflight/v2AllowanceRequirements.test.ts @@ -1,6 +1,7 @@ import { PERMIT2_ADDRESS } from '../safe/constants'; import { POLYMARKET_V2_PROTOCOL } from '../protocol/definitions'; import { + filterDepositWalletUnsupportedRequirements, getActiveV2AllowanceRequirements, getCanonicalV2AllowanceRequirements, } from './v2AllowanceRequirements'; @@ -39,4 +40,32 @@ describe('v2 allowance requirements', () => { ]), ); }); + + it('filters Permit2 approvals from deposit-wallet requirements', () => { + const requirements = getActiveV2AllowanceRequirements(); + const filteredRequirements = + filterDepositWalletUnsupportedRequirements(requirements); + + expect(filteredRequirements).toHaveLength(requirements.length - 1); + expect(filteredRequirements).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: 'erc20-allowance', + spender: PERMIT2_ADDRESS, + }), + ]), + ); + expect(filteredRequirements).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: 'erc20-allowance', + spender: POLYMARKET_V2_PROTOCOL.contracts.exchange, + }), + expect.objectContaining({ + type: 'erc1155-operator', + operator: POLYMARKET_V2_PROTOCOL.contracts.exchange, + }), + ]), + ); + }); }); diff --git a/app/components/UI/Predict/providers/polymarket/preflight/v2AllowanceRequirements.ts b/app/components/UI/Predict/providers/polymarket/preflight/v2AllowanceRequirements.ts index b83b7538777..a25261da760 100644 --- a/app/components/UI/Predict/providers/polymarket/preflight/v2AllowanceRequirements.ts +++ b/app/components/UI/Predict/providers/polymarket/preflight/v2AllowanceRequirements.ts @@ -90,6 +90,21 @@ export function getActiveV2AllowanceRequirements( ]; } +export function filterDepositWalletUnsupportedRequirements( + requirements: V2AllowanceRequirement[], +): V2AllowanceRequirement[] { + return requirements.filter((requirement) => { + if (requirement.type !== 'erc20-allowance') { + return true; + } + + // Polymarket's deposit-wallet relayer allow-list blocks Permit2 approvals + // (`approve spender 0x000000000022D473030F116dDEE9F6B43aC78BA3 is not in the allowed list`). + // Skip this for deposit-wallet setup/claims; fees use a different flow. + return requirement.spender.toLowerCase() !== PERMIT2_ADDRESS.toLowerCase(); + }); +} + export function getCanonicalV2AllowanceRequirements( protocol: PolymarketProtocolDefinition = POLYMARKET_V2_PROTOCOL, ): V2AllowanceRequirement[] { diff --git a/app/components/UI/Predict/providers/polymarket/protocol/orderCodec.test.ts b/app/components/UI/Predict/providers/polymarket/protocol/orderCodec.test.ts index 7bd4f2fb5f8..dfa5ad94594 100644 --- a/app/components/UI/Predict/providers/polymarket/protocol/orderCodec.test.ts +++ b/app/components/UI/Predict/providers/polymarket/protocol/orderCodec.test.ts @@ -1,5 +1,9 @@ +import { SignTypedDataVersion } from '@metamask/keyring-controller'; +import { hexlify, toUtf8Bytes } from 'ethers/lib/utils'; import { Side, type OrderPreview } from '../../../types'; -import { OrderType } from '../types'; +import type { Signer } from '../../types'; +import { HASH_ZERO_BYTES32, POLYGON_MAINNET_CHAIN_ID } from '../constants'; +import { OrderType, SignatureType } from '../types'; import { POLYMARKET_V2_PROTOCOL } from './definitions'; import { buildProtocolUnsignedOrder, @@ -8,6 +12,7 @@ import { getProtocolOrderTypedData, getProtocolVerifyingContract, serializeProtocolRelayerOrder, + signProtocolOrder, } from './orderCodec'; const preview: OrderPreview = { @@ -26,6 +31,24 @@ const preview: OrderPreview = { feeRateBps: '77', }; +const ORDER_TYPE_STRING = + 'Order(uint256 salt,address maker,address signer,uint256 tokenId,uint256 makerAmount,uint256 takerAmount,uint8 side,uint8 signatureType,uint256 timestamp,bytes32 metadata,bytes32 builder)'; +const rawSignature = `0x${'11'.repeat(65)}`; +const ownerAddress = '0x1111111111111111111111111111111111111111'; +const safeAddress = '0x2222222222222222222222222222222222222222'; +const depositWalletAddress = '0x3333333333333333333333333333333333333333'; + +function createMockSigner(signature = rawSignature) { + const signTypedMessage = jest.fn().mockResolvedValue(signature); + const signer: Signer = { + address: ownerAddress, + signTypedMessage, + signPersonalMessage: jest.fn(), + }; + + return { signer, signTypedMessage }; +} + describe('polymarket protocol order codec', () => { const protocol = { ...POLYMARKET_V2_PROTOCOL, @@ -36,12 +59,12 @@ describe('polymarket protocol order codec', () => { }, }; - it('builds a v2 order with timestamp, metadata, and builder', () => { + it('builds a v2 order with timestamp, metadata, builder, and Safe signature type', () => { const order = buildProtocolUnsignedOrder({ protocol, preview, - makerAddress: '0x1111111111111111111111111111111111111111', - signerAddress: '0x2222222222222222222222222222222222222222', + makerAddress: ownerAddress, + signerAddress: safeAddress, nowInSeconds: 456, }); @@ -54,6 +77,7 @@ describe('polymarket protocol order codec', () => { 'builder', '0x3333333333333333333333333333333333333333333333333333333333333333', ); + expect(order.signatureType).toBe(SignatureType.POLY_GNOSIS_SAFE); expect(order).not.toHaveProperty('taker'); expect(order).not.toHaveProperty('nonce'); expect(order).not.toHaveProperty('feeRateBps'); @@ -73,12 +97,28 @@ describe('polymarket protocol order codec', () => { ]); }); + it('builds a deposit-wallet order with POLY_1271 signature type', () => { + const order = buildProtocolUnsignedOrder({ + protocol, + preview, + makerAddress: depositWalletAddress, + signerAddress: depositWalletAddress, + signatureType: SignatureType.POLY_1271, + nowInSeconds: 456, + }); + + expect(SignatureType.POLY_1271).toBe(3); + expect(order.maker).toBe(depositWalletAddress); + expect(order.signer).toBe(depositWalletAddress); + expect(order.signatureType).toBe(SignatureType.POLY_1271); + }); + it('builds v2 typed data with domain version 2 and bytes32 fields', () => { const order = buildProtocolUnsignedOrder({ protocol, preview, - makerAddress: '0x1111111111111111111111111111111111111111', - signerAddress: '0x2222222222222222222222222222222222222222', + makerAddress: ownerAddress, + signerAddress: safeAddress, nowInSeconds: 456, }); @@ -103,12 +143,112 @@ describe('polymarket protocol order codec', () => { ); }); + it('signs Safe orders with the raw CLOB Order typed data', async () => { + const { signer, signTypedMessage } = createMockSigner(); + const order = buildProtocolUnsignedOrder({ + protocol, + preview, + makerAddress: safeAddress, + signerAddress: ownerAddress, + nowInSeconds: 456, + }); + const verifyingContract = getProtocolVerifyingContract({ + protocol, + negRisk: false, + }); + + await expect( + signProtocolOrder({ signer, protocol, order, verifyingContract }), + ).resolves.toBe(rawSignature); + + expect(signTypedMessage).toHaveBeenCalledWith( + { + from: ownerAddress, + data: expect.objectContaining({ + primaryType: 'Order', + domain: expect.objectContaining({ + name: 'Polymarket CTF Exchange', + version: '2', + verifyingContract, + }), + message: order, + }), + }, + SignTypedDataVersion.V4, + ); + }); + + it('signs deposit-wallet orders with an ERC-7739 TypedDataSign wrapper', async () => { + const { signer, signTypedMessage } = createMockSigner(); + const order = buildProtocolUnsignedOrder({ + protocol, + preview, + makerAddress: depositWalletAddress, + signerAddress: depositWalletAddress, + signatureType: SignatureType.POLY_1271, + nowInSeconds: 456, + }); + const verifyingContract = getProtocolVerifyingContract({ + protocol, + negRisk: false, + }); + + const signature = await signProtocolOrder({ + signer, + protocol, + order, + verifyingContract, + }); + + const expectedContentsTypeSuffix = `${hexlify( + toUtf8Bytes(ORDER_TYPE_STRING), + ).slice(2)}${ORDER_TYPE_STRING.length.toString(16).padStart(4, '0')}`; + + expect(signature.startsWith(rawSignature)).toBe(true); + expect(signature.endsWith(expectedContentsTypeSuffix)).toBe(true); + expect(signature.length).toBe( + rawSignature.length + 32 * 2 + 32 * 2 + expectedContentsTypeSuffix.length, + ); + expect(signTypedMessage).toHaveBeenCalledWith( + { + from: ownerAddress, + data: expect.objectContaining({ + primaryType: 'TypedDataSign', + domain: expect.objectContaining({ + name: 'Polymarket CTF Exchange', + version: '2', + chainId: POLYGON_MAINNET_CHAIN_ID, + verifyingContract, + }), + types: expect.objectContaining({ + TypedDataSign: expect.arrayContaining([ + { name: 'contents', type: 'Order' }, + { name: 'verifyingContract', type: 'address' }, + ]), + Order: expect.arrayContaining([ + { name: 'signatureType', type: 'uint8' }, + ]), + }), + message: { + contents: order, + name: 'DepositWallet', + version: '1', + chainId: POLYGON_MAINNET_CHAIN_ID, + verifyingContract: depositWalletAddress, + salt: HASH_ZERO_BYTES32, + }, + }), + }, + SignTypedDataVersion.V4, + ); + }); + it('serializes signed orders into the relayer body shape', () => { const order = buildProtocolUnsignedOrder({ protocol, preview, - makerAddress: '0x1111111111111111111111111111111111111111', - signerAddress: '0x2222222222222222222222222222222222222222', + makerAddress: ownerAddress, + signerAddress: safeAddress, nowInSeconds: 456, }); @@ -133,6 +273,8 @@ describe('polymarket protocol order codec', () => { }), }), ); + expect(serialized).not.toHaveProperty('deferExec'); + expect(serialized).not.toHaveProperty('postOnly'); }); it('forces preview fee rate to zero', () => { @@ -143,7 +285,7 @@ describe('polymarket protocol order codec', () => { expect( encodeWrap({ asset: protocol.collateral.legacyUsdceToken, - to: '0x1111111111111111111111111111111111111111', + to: ownerAddress, amount: 42n, }), ).toMatch(/^0x[0-9a-f]+$/u); diff --git a/app/components/UI/Predict/providers/polymarket/protocol/orderCodec.ts b/app/components/UI/Predict/providers/polymarket/protocol/orderCodec.ts index 5f77591c9df..fbac7c0b36a 100644 --- a/app/components/UI/Predict/providers/polymarket/protocol/orderCodec.ts +++ b/app/components/UI/Predict/providers/polymarket/protocol/orderCodec.ts @@ -1,8 +1,18 @@ +import { SignTypedDataVersion } from '@metamask/keyring-controller'; import { Hex } from '@metamask/utils'; -import { Interface, parseUnits } from 'ethers/lib/utils'; +import { + defaultAbiCoder, + hexlify, + Interface, + keccak256, + parseUnits, + toUtf8Bytes, +} from 'ethers/lib/utils'; import { Side, type OrderPreview } from '../../../types'; +import type { Signer } from '../../types'; import { EIP712Domain, + HASH_ZERO_BYTES32, POLYGON_MAINNET_CHAIN_ID, ROUNDING_CONFIG, } from '../constants'; @@ -51,10 +61,32 @@ export type ProtocolRelayerOrder = ClobOrderObjectV2; const ORDER_PRIMARY_TYPE = 'Order'; const ORDER_DOMAIN_NAME = 'Polymarket CTF Exchange'; +const DEPOSIT_WALLET_DOMAIN_NAME = 'DepositWallet'; +const DEPOSIT_WALLET_DOMAIN_VERSION = '1'; +const ORDER_TYPE_STRING = + 'Order(uint256 salt,address maker,address signer,uint256 tokenId,uint256 makerAmount,uint256 takerAmount,uint8 side,uint8 signatureType,uint256 timestamp,bytes32 metadata,bytes32 builder)'; +const ORDER_TYPE_HASH = keccak256(toUtf8Bytes(ORDER_TYPE_STRING)); +const DOMAIN_TYPE_HASH = keccak256( + toUtf8Bytes( + 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)', + ), +); const ORDER_DOMAIN_TYPES = [ ...EIP712Domain, { name: 'verifyingContract', type: 'address' }, ]; +const TYPED_DATA_SIGN_STRUCT = [ + { name: 'contents', type: ORDER_PRIMARY_TYPE }, + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' }, + { name: 'chainId', type: 'uint256' }, + { name: 'verifyingContract', type: 'address' }, + { name: 'salt', type: 'bytes32' }, +]; + +function withoutHexPrefix(value: string): string { + return value.startsWith('0x') ? value.slice(2) : value; +} function buildProtocolOrderDomain({ protocol, @@ -120,12 +152,14 @@ export function buildProtocolUnsignedOrder({ preview, makerAddress, signerAddress, + signatureType = SignatureType.POLY_GNOSIS_SAFE, nowInSeconds = Math.floor(Date.now() / 1000), }: { protocol: PolymarketProtocolDefinition; preview: OrderPreview; makerAddress: string; signerAddress: string; + signatureType?: SignatureType; nowInSeconds?: number; }): ProtocolUnsignedOrder { // NOTE: Field order matters for EIP-712 signing. Do NOT use object spread @@ -140,7 +174,6 @@ export function buildProtocolUnsignedOrder({ ).toString(); const takerAmount = getTakerAmountWithSlippage(preview); const side = preview.side === Side.BUY ? UtilsSide.BUY : UtilsSide.SELL; - const signatureType = SignatureType.POLY_GNOSIS_SAFE; const builder = protocol.order.getBuilderCode(); if (!builder) { @@ -198,6 +231,147 @@ export function getProtocolOrderTypedData({ }; } +function getProtocolAppDomainSeparator({ + protocol, + verifyingContract, + chainId, +}: { + protocol: PolymarketProtocolDefinition; + verifyingContract: string; + chainId: number; +}): string { + return keccak256( + defaultAbiCoder.encode( + ['bytes32', 'bytes32', 'bytes32', 'uint256', 'address'], + [ + DOMAIN_TYPE_HASH, + keccak256(toUtf8Bytes(ORDER_DOMAIN_NAME)), + keccak256(toUtf8Bytes(protocol.order.domainVersion)), + chainId, + verifyingContract, + ], + ), + ); +} + +function getProtocolOrderContentsHash(order: ProtocolUnsignedOrder): string { + if (!order.signer) { + throw new Error('Missing Polymarket CLOB order signer'); + } + + if (order.signatureType === undefined) { + throw new Error('Missing Polymarket CLOB order signature type'); + } + + return keccak256( + defaultAbiCoder.encode( + [ + 'bytes32', + 'uint256', + 'address', + 'address', + 'uint256', + 'uint256', + 'uint256', + 'uint8', + 'uint8', + 'uint256', + 'bytes32', + 'bytes32', + ], + [ + ORDER_TYPE_HASH, + BigInt(order.salt).toString(), + order.maker, + order.signer, + BigInt(order.tokenId).toString(), + BigInt(order.makerAmount).toString(), + BigInt(order.takerAmount).toString(), + order.side, + order.signatureType, + BigInt(order.timestamp).toString(), + order.metadata, + order.builder, + ], + ), + ); +} + +export async function signProtocolOrder({ + signer, + protocol, + order, + verifyingContract, + chainId = POLYGON_MAINNET_CHAIN_ID, +}: { + signer: Signer; + protocol: PolymarketProtocolDefinition; + order: ProtocolUnsignedOrder; + verifyingContract: string; + chainId?: number; +}): Promise { + const typedData = getProtocolOrderTypedData({ + protocol, + order, + verifyingContract, + chainId, + }); + + if (order.signatureType !== SignatureType.POLY_1271) { + return signer.signTypedMessage( + { data: typedData, from: signer.address }, + SignTypedDataVersion.V4, + ); + } + + if (!order.signer) { + throw new Error('Missing Polymarket deposit wallet signer'); + } + + const innerSignature = await signer.signTypedMessage( + { + from: signer.address, + data: { + primaryType: 'TypedDataSign', + domain: typedData.domain, + types: { + EIP712Domain: ORDER_DOMAIN_TYPES, + TypedDataSign: TYPED_DATA_SIGN_STRUCT, + Order: typedData.types.Order, + }, + message: { + contents: typedData.message, + name: DEPOSIT_WALLET_DOMAIN_NAME, + version: DEPOSIT_WALLET_DOMAIN_VERSION, + chainId, + verifyingContract: order.signer, + salt: HASH_ZERO_BYTES32, + }, + }, + }, + SignTypedDataVersion.V4, + ); + + const appDomainSeparator = getProtocolAppDomainSeparator({ + protocol, + verifyingContract, + chainId, + }); + const contentsHash = getProtocolOrderContentsHash(order); + const contentsTypeHex = withoutHexPrefix( + hexlify(toUtf8Bytes(ORDER_TYPE_STRING)), + ); + const contentsTypeLengthHex = ORDER_TYPE_STRING.length + .toString(16) + .padStart(4, '0'); + + return `0x${withoutHexPrefix(innerSignature)}${withoutHexPrefix( + appDomainSeparator, + )}${withoutHexPrefix( + contentsHash, + )}${contentsTypeHex}${contentsTypeLengthHex}`; +} + export function serializeProtocolRelayerOrder({ signedOrder, owner, diff --git a/app/components/UI/Predict/providers/polymarket/types.ts b/app/components/UI/Predict/providers/polymarket/types.ts index a06b84a6379..b97517f333a 100644 --- a/app/components/UI/Predict/providers/polymarket/types.ts +++ b/app/components/UI/Predict/providers/polymarket/types.ts @@ -172,6 +172,11 @@ export enum SignatureType { * EIP712 signatures signed by EOAs that own Polymarket Gnosis safes */ POLY_GNOSIS_SAFE, + + /** + * ERC-1271 signatures validated by Polymarket deposit wallets + */ + POLY_1271, } // Simplified market order for users diff --git a/app/components/UI/Predict/providers/types.ts b/app/components/UI/Predict/providers/types.ts index 17803ff6e59..7684f45261b 100644 --- a/app/components/UI/Predict/providers/types.ts +++ b/app/components/UI/Predict/providers/types.ts @@ -60,6 +60,7 @@ export interface PrepareDepositParams { export interface GetAccountStateParams { ownerAddress: string; + forceRefresh?: boolean; } export interface PrepareWithdrawParams { diff --git a/app/components/UI/Predict/types/index.ts b/app/components/UI/Predict/types/index.ts index 39970b2c497..fa4e975816d 100644 --- a/app/components/UI/Predict/types/index.ts +++ b/app/components/UI/Predict/types/index.ts @@ -612,9 +612,12 @@ export interface PreviewOrderParams { positionId?: string; } +export type PredictWalletType = 'safe' | 'deposit-wallet'; + export interface AccountState { address: Hex; isDeployed: boolean; + walletType: PredictWalletType; } export interface GeoBlockResponse { @@ -635,8 +638,9 @@ export type CryptoPriceUpdateCallback = (update: CryptoPriceUpdate) => void; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface PrepareDepositParams {} -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface GetAccountStateParams {} +export interface GetAccountStateParams { + forceRefresh?: boolean; +} // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface PrepareWithdrawParams {} diff --git a/app/components/hooks/useMultichainBalances/useMultichainBalances.types.ts b/app/components/hooks/useMultichainBalances/useMultichainBalances.types.ts index 23e560d9ae2..56f38724302 100644 --- a/app/components/hooks/useMultichainBalances/useMultichainBalances.types.ts +++ b/app/components/hooks/useMultichainBalances/useMultichainBalances.types.ts @@ -1,7 +1,13 @@ import { InternalAccount } from '@metamask/keyring-internal-api'; -import { AggregatedPercentageProps } from '../../../component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.types'; import { AggregatedPercentageCrossChainsProps } from '../../../component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentageCrossChains.types'; +export interface AggregatedBalance { + ethFiat: number; + tokenFiat: number; + tokenFiat1dAgo: number; + ethFiat1dAgo: number; +} + export interface MultichainBalancesData { displayBalance?: string; displayCurrency: string; @@ -11,7 +17,7 @@ export interface MultichainBalancesData { nativeTokenUnit: string; shouldShowAggregatedPercentage: boolean; isPortfolioViewEnabled: boolean; - aggregatedBalance: AggregatedPercentageProps; + aggregatedBalance: AggregatedBalance; isLoadingAccount: boolean; } diff --git a/app/components/hooks/useMultichainBalances/utils.ts b/app/components/hooks/useMultichainBalances/utils.ts index 7c7bfd33d2d..6d12036d18b 100644 --- a/app/components/hooks/useMultichainBalances/utils.ts +++ b/app/components/hooks/useMultichainBalances/utils.ts @@ -18,6 +18,7 @@ import Engine from '../../../core/Engine'; import { SupportedCaipChainId } from '@metamask/multichain-network-controller'; import { TotalFiatBalancesCrossChains } from '../useGetTotalFiatBalanceCrossChains'; import { isTestNet } from '../../../util/networks'; +import type { AggregatedBalance } from './useMultichainBalances.types'; // Production balance calculation (EVM) const getEvmBalance = ( @@ -89,7 +90,9 @@ export const getShouldShowAggregatedPercentage = ( return !isTestNet(chainId); }; -export const getAggregatedBalance = (account: InternalAccount) => { +export const getAggregatedBalance = ( + account: InternalAccount, +): AggregatedBalance => { const balance = Engine.getTotalEvmFiatAccountBalance(account); return { ethFiat: balance?.ethFiat ?? 0, diff --git a/app/core/Engine/controllers/transaction-controller/transaction-controller-init.test.ts b/app/core/Engine/controllers/transaction-controller/transaction-controller-init.test.ts index 7d77de82cfe..d811d27d661 100644 --- a/app/core/Engine/controllers/transaction-controller/transaction-controller-init.test.ts +++ b/app/core/Engine/controllers/transaction-controller/transaction-controller-init.test.ts @@ -92,10 +92,20 @@ const MOCK_TRANSACTION_META = { * with the default mock. * @returns A mock NetworkController. */ +type ControllerMock = NetworkController & { + beforePublish: jest.Mock; + beforeSign: jest.Mock; + publish: jest.Mock; +}; + function buildControllerMock( - partialMock?: Partial, -): NetworkController { - const defaultControllerMocks = {}; + partialMock?: Partial, +): ControllerMock { + const defaultControllerMocks = { + beforePublish: jest.fn().mockResolvedValue(true), + beforeSign: jest.fn(), + publish: jest.fn().mockResolvedValue({ transactionHash: undefined }), + }; // @ts-expect-error Incomplete mock, just includes properties used by code-under-test. return { @@ -112,22 +122,43 @@ function buildInitRequestMock( TransactionControllerInitMessenger > > { + const { + predictControllerMock: providedPredictControllerMock, + ...requestOverrides + } = initRequestProperties; + const predictControllerMock = + (providedPredictControllerMock as ControllerMock | undefined) ?? + buildControllerMock(); const initMessenger = new ExtendedMessenger({ namespace: MOCK_ANY_NAMESPACE, }); const baseControllerMessenger = new ExtendedMessenger({ namespace: MOCK_ANY_NAMESPACE, }); + (initMessenger as unknown as { call: jest.Mock }).call = jest.fn( + (actionType: string, params: unknown) => { + if (actionType === 'PredictController:beforePublish') { + return predictControllerMock.beforePublish(params); + } + + if (actionType === 'PredictController:publish') { + return predictControllerMock.publish(params); + } + + throw new Error(`Unexpected init messenger action: ${actionType}`); + }, + ); + const requestMock = { ...buildMessengerClientInitRequestMock(baseControllerMessenger), initMessenger: initMessenger as unknown as TransactionControllerInitMessenger, controllerMessenger: baseControllerMessenger as unknown as TransactionControllerMessenger, - ...initRequestProperties, + ...requestOverrides, }; - if (!initRequestProperties.getMessengerClient) { + if (!requestOverrides.getMessengerClient) { requestMock.getMessengerClient.mockReturnValue(buildControllerMock()); } @@ -180,9 +211,11 @@ describe('Transaction Controller Init', () => { ): TransactionControllerOptions[T] { const requestMock = buildInitRequestMock(initRequestProperties); - requestMock.getMessengerClient.mockReturnValue( - buildControllerMock(dependencyProperties), - ); + if (!initRequestProperties.getMessengerClient) { + requestMock.getMessengerClient.mockReturnValue( + buildControllerMock(dependencyProperties), + ); + } TransactionControllerInit(requestMock); @@ -320,6 +353,25 @@ describe('Transaction Controller Init', () => { expect(optionFn?.()).toBe(false); }); + describe('beforePublish hook', () => { + it('delegates to PredictController beforePublish', async () => { + const predictControllerMock = buildControllerMock(); + const hooks = testConstructorOption( + 'hooks', + {}, + { + predictControllerMock, + }, + ); + + await hooks?.beforePublish?.(MOCK_TRANSACTION_META); + + expect(predictControllerMock.beforePublish).toHaveBeenCalledWith({ + transactionMeta: MOCK_TRANSACTION_META, + }); + }); + }); + describe('publish hook', () => { it('calls submitSmartTransactionHook', async () => { const hooks = testConstructorOption('hooks'); @@ -347,6 +399,53 @@ describe('Transaction Controller Init', () => { expect(payHookMock).toHaveBeenCalledTimes(1); }); + it('calls Predict publish before pay and smart transaction hooks', async () => { + const predictControllerMock = buildControllerMock(); + const hooks = testConstructorOption( + 'hooks', + {}, + { + predictControllerMock, + }, + ); + + await hooks?.publish?.(MOCK_TRANSACTION_META); + + expect(predictControllerMock.publish).toHaveBeenCalledWith({ + transactionMeta: MOCK_TRANSACTION_META, + }); + expect(payHookMock).toHaveBeenCalledTimes(1); + expect( + (predictControllerMock.publish as jest.Mock).mock + .invocationCallOrder[0], + ).toBeLessThan(payHookMock.mock.invocationCallOrder[0]); + expect( + (predictControllerMock.publish as jest.Mock).mock + .invocationCallOrder[0], + ).toBeLessThan( + submitSmartTransactionHookMock.mock.invocationCallOrder[0], + ); + }); + + it('short-circuits publish when Predict returns a transaction hash', async () => { + const predictControllerMock = buildControllerMock({ + publish: jest.fn().mockResolvedValue({ transactionHash: '0xpredict' }), + } as unknown as Partial); + const hooks = testConstructorOption( + 'hooks', + {}, + { + predictControllerMock, + }, + ); + + const result = await hooks?.publish?.(MOCK_TRANSACTION_META); + + expect(result).toEqual({ transactionHash: '0xpredict' }); + expect(payHookMock).not.toHaveBeenCalled(); + expect(submitSmartTransactionHookMock).not.toHaveBeenCalled(); + }); + it('passes isSmartTransaction returning false to pay hook when stxDisabled is true', async () => { selectMetaMaskPayFlagsMock.mockReturnValue({ attemptsMax: 2, diff --git a/app/core/Engine/controllers/transaction-controller/transaction-controller-init.ts b/app/core/Engine/controllers/transaction-controller/transaction-controller-init.ts index 64bc20e25cb..e4757003edd 100644 --- a/app/core/Engine/controllers/transaction-controller/transaction-controller-init.ts +++ b/app/core/Engine/controllers/transaction-controller/transaction-controller-init.ts @@ -136,6 +136,8 @@ export const TransactionControllerInit: MessengerClientInitFunction< transactions: _request.transactions as PublishBatchHookTransaction[], }), + beforePublish: (transactionMeta: TransactionMeta) => + beforePublish(transactionMeta, initMessenger), beforeSign: (_request: { transactionMeta: TransactionMeta }) => beforeSign(_request, request), }, @@ -226,6 +228,15 @@ async function publishHook({ initMessenger: TransactionControllerInitMessenger; signedTransactionInHex: Hex; }): Promise<{ transactionHash?: string }> { + const { transactionHash: predictTransactionHash } = await initMessenger.call( + 'PredictController:publish', + { transactionMeta }, + ); + + if (predictTransactionHash) { + return { transactionHash: predictTransactionHash }; + } + const state = getState(); const { shouldUseSmartTransaction, featureFlags } = @@ -441,6 +452,15 @@ function getControllers( }; } +function beforePublish( + transactionMeta: TransactionMeta, + initMessenger: TransactionControllerInitMessenger, +) { + return initMessenger.call('PredictController:beforePublish', { + transactionMeta, + }); +} + function beforeSign( hookRequest: { transactionMeta: TransactionMeta }, request: MessengerClientInitRequest< diff --git a/app/core/Engine/messengers/transaction-controller-messenger/transaction-controller-messenger.ts b/app/core/Engine/messengers/transaction-controller-messenger/transaction-controller-messenger.ts index ca6bb67d57f..324fbdd8ccc 100644 --- a/app/core/Engine/messengers/transaction-controller-messenger/transaction-controller-messenger.ts +++ b/app/core/Engine/messengers/transaction-controller-messenger/transaction-controller-messenger.ts @@ -55,6 +55,10 @@ import { MessengerActions, MessengerEvents, } from '@metamask/messenger'; +import type { + PredictControllerBeforePublishAction, + PredictControllerPublishAction, +} from '../../../../components/UI/Predict/controllers/PredictController-method-action-types'; export function getTransactionControllerMessenger( rootMessenger: RootMessenger, @@ -114,7 +118,9 @@ type InitMessengerActions = | TransactionPayControllerGetDelegationTransactionAction | TransactionPayControllerGetStateAction | TransactionPayControllerGetStrategyAction - | AnalyticsControllerActions; + | AnalyticsControllerActions + | PredictControllerBeforePublishAction + | PredictControllerPublishAction; type InitMessengerEvents = | BridgeStatusControllerEvents @@ -173,6 +179,8 @@ export function getTransactionControllerInitMessenger( 'TransactionPayController:getState', 'TransactionPayController:getStrategy', 'AnalyticsController:trackEvent', + 'PredictController:beforePublish', + 'PredictController:publish', ], events: [ 'BridgeStatusController:stateChange', diff --git a/builds.yml b/builds.yml index bc9e53a3686..4888d01c6c5 100644 --- a/builds.yml +++ b/builds.yml @@ -231,7 +231,7 @@ builds: PREDEFINED_PASSWORD: 'E2E_PASSWORD' code_fencing: *code_fencing_main - # Test builds (QA) + # Test builds main-test: github_environment: build-uat signing: *signing_uat @@ -373,7 +373,7 @@ builds: secrets: *secrets code_fencing: *code_fencing_flask - # Flask test builds (QA) + # Flask test builds flask-test: github_environment: build-flask-uat signing: *signing_flask_uat diff --git a/docs/bigint-migration-guide.md b/docs/bigint-migration-guide.md index 6028547edf9..c7e87faa1c8 100644 --- a/docs/bigint-migration-guide.md +++ b/docs/bigint-migration-guide.md @@ -92,8 +92,6 @@ The table below maps each burndown path to the GitHub team(s) from [`.github/COD - `app/component-library/components-temp/CustomSpendCap/CustomInput/CustomInput.tsx` - `app/component-library/components-temp/CustomSpendCap/CustomSpendCap.tsx` -- `app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.tsx` -- `app/component-library/components-temp/Price/AggregatedPercentage/utils.ts` ### @MetaMask/earn diff --git a/docs/readme/e2e-testing.md b/docs/readme/e2e-testing.md index 84350576545..885b47df64b 100644 --- a/docs/readme/e2e-testing.md +++ b/docs/readme/e2e-testing.md @@ -357,174 +357,6 @@ yarn test:e2e:android:flask:run --- -
-Legacy Appium Documentation (for reference only) - -**Supported Platform**: Android -**Test Location**: `wdio` - -## Configuration for Testing - -We have two separate configurations for testing the different variants of our applications: - -- **QA Variant (local)**: Runs in debug mode on your local machine. -- **QA Variant (production)**: Runs in production mode on BrowserStack. - -We use the QA variant for Appium tests because of our screen-blocking mechanism, which would otherwise prevent tests from getting past the wallet setup screen. - -### Capabilities Setup - -We require two sets of capabilities to handle app upgrade tests, leading to the creation of two configurations: `defaultCapabilities` and `upgradeCapabilities`. - -#### Default Capabilities - -```javascript -const defaultCapabilities = [ - { - platformName: 'Android', - noReset: false, - fullReset: false, - maxInstances: 1, - build: 'Android App Launch Times Tests', - device: process.env.BROWSERSTACK_DEVICE || 'Google Pixel 6', - os_version: process.env.BROWSERSTACK_OS_VERSION || '12.0', - app: process.env.BROWSERSTACK_APP_URL, - 'browserstack.debug': true, - 'browserstack.local': true, - }, -]; -``` - -This configuration is our standard, as it only requires one app per install. - -#### Upgrade Capabilities - -```javascript -const upgradeCapabilities = [ - { - platformName: 'Android', - noReset: false, - fullReset: false, - maxInstances: 1, - build: 'Android App Upgrade Tests', - device: process.env.BROWSERSTACK_DEVICE || 'Google Pixel 6', - os_version: process.env.BROWSERSTACK_OS_VERSION || '12.0', - app: process.env.PRODUCTION_APP_URL || process.env.BROWSERSTACK_APP_URL, - 'browserstack.debug': true, - 'browserstack.local': true, - 'browserstack.midSessionInstallApps': [process.env.BROWSERSTACK_APP_URL], - }, -]; -``` - -This configuration requires two applications: the current production app and the app built from the branch. - -**Note**: You can, if you choose to, run the tests against any one of the devices and operating systems mentioned in the browserstack device [list](https://www.browserstack.com/list-of-browsers-and-platforms/app_automate). - -### Flag-Based Capability Selection - -We use flags like `--performance` and `--upgrade` to determine which capabilities to use for specific tests. - -```javascript -const { selectedCapabilities, defaultTagExpression } = (() => { - if (isAppUpgrade) { - return { - selectedCapabilities: upgradeCapabilities, - defaultTagExpression: '@upgrade and @androidApp', - }; - } else if (isPerformance) { - return { - selectedCapabilities: defaultCapabilities, - defaultTagExpression: '@performance and @androidApp', - }; - } else { - return { - selectedCapabilities: defaultCapabilities, - defaultTagExpression: '@smoke and @androidApp', - }; - } -})(); -``` - -## Running Tests Locally Against QA Build - -You can run your E2E tests on local simulators either in development mode (with automatic code refresh) or without it. - -Install dependencies: - -```bash -yarn setup -``` - -Ensure that the bundler compiles all files before running the tests to avoid build breaks. Use: - -```bash -yarn watch:clean -``` - -### iOS - -To start an iOS QA build: - -```bash -yarn start:ios:qa -``` - -### Android - -To start an Android QA build: - -```bash -yarn start:android:qa -``` - -Then, run the tests on the simulator: - -### iOS - -```bash -yarn test:wdio:ios -``` - -### Android - -```bash -yarn test:wdio:android -``` - -To run specific tests, use the `--spec` option: - -```bash -yarn test:wdio:android --spec ./wdio/features/performance/ColdStartLaunchTimes.feature -``` - -**Note**: Ensure that your installed simulator names match the configurations in `wdio/config/android.config.debug.js` and `wdio/config/ios.config.debug.js`. - -## Running Tests on BrowserStack - -To trigger tests locally on BrowserStack: - -1. Retrieve your BrowserStack username and access key from the App Automate section. -2. Update `config.user` and `config.key` in `android.config.browserstack` with your BrowserStack credentials. -3. Upload your app to BrowserStack via the `create_qa_builds_pipeline`. Grab the `app_url` from `browserstack_uploaded_apps.json`. -4. Update `process.env.BROWSERSTACK_APP_URL` with the correct `app_url`. -5. Run your tests using the appropriate flag (e.g., for performance tests): - -```bash -yarn test:wdio:android:browserstack --performance -``` - -## Running Appium Tests on CI (Bitrise) - -You can also run Appium tests on CI using Bitrise pipelines: - -- `app_launch_times_pipeline` -- `app_upgrade_pipeline` - -For more details on our CI pipelines, see the [Bitrise Pipelines Overview](#bitrise-pipelines-overview). - -
- ### API Spec Tests **Platform**: iOS @@ -569,12 +401,12 @@ Do **not** set `BROWSERSTACK_LOCAL=true` unless the tunnel is running. Other Bro Update the config file with the appropriate BrowserStack app URL. You’ll need a BrowserStack URL first. To get it: -1. Run `create_qa_builds_pipeline` on Bitrise -2. Once done, open the **Artifacts** tab and find `browserstack_uploaded_apps.json` (from `build_android_qa` and `build_ios_qa`). +1. Run [Build Mobile App](https://github.com/MetaMask/metamask-mobile/actions/workflows/build.yml) in Github Actions. +2. Once done, open scroll down to the **Artifacts** section in the workflow and find the build artifacts. -See this [build](https://app.bitrise.io/app/be69d4368ee7e86d/pipelines/de2bf4ee-b000-4a7c-bd5b-c995ae0f3b4d?tab=artifacts) as an example. +See this [workflow](https://github.com/MetaMask/metamask-mobile/actions/runs/25391553223) as an example. -The first entry in that JSON will include your app’s URL (look for the bs:// prefix). +Download the build artifact and upload it to BrowserStack App Automate service. Once the upload is complete, it will provide a BrowserStack URL that you can copy. Add it to the config file by replacing `process.env.BROWSERSTACK_ANDROID_APP_URL` in the `buildPath` with the appropriate BrowserStack application URL: diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index f29b89d59c1..445cffdce22 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -33,68 +33,41 @@ 2EF2827D2B0FF86900D7B4B1 /* branch.json in Resources */ = {isa = PBXBuildFile; fileRef = FE3C9A2458A1416290DEDAD4 /* branch.json */; }; 2EF2828C2B0FF86900D7B4B1 /* Branch.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 153F84C92319B8DB00C19B63 /* Branch.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 2EF2832A2B17EBD600D7B4B1 /* RnTar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EF283292B17EBD600D7B4B1 /* RnTar.swift */; }; - 2EF2832B2B17EBD600D7B4B1 /* RnTar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EF283292B17EBD600D7B4B1 /* RnTar.swift */; }; 2EF2832C2B17EBD600D7B4B1 /* RnTar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EF283292B17EBD600D7B4B1 /* RnTar.swift */; }; 2EF283322B17EC1A00D7B4B1 /* RNTar.m in Sources */ = {isa = PBXBuildFile; fileRef = 2EF283312B17EC1A00D7B4B1 /* RNTar.m */; }; - 2EF283332B17EC1A00D7B4B1 /* RNTar.m in Sources */ = {isa = PBXBuildFile; fileRef = 2EF283312B17EC1A00D7B4B1 /* RNTar.m */; }; 2EF283342B17EC1A00D7B4B1 /* RNTar.m in Sources */ = {isa = PBXBuildFile; fileRef = 2EF283312B17EC1A00D7B4B1 /* RNTar.m */; }; 2EF283372B17EC7900D7B4B1 /* Light-Swift-Untar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EF283362B17EC7900D7B4B1 /* Light-Swift-Untar.swift */; }; - 2EF283382B17EC7900D7B4B1 /* Light-Swift-Untar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EF283362B17EC7900D7B4B1 /* Light-Swift-Untar.swift */; }; 2EF283392B17EC7900D7B4B1 /* Light-Swift-Untar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EF283362B17EC7900D7B4B1 /* Light-Swift-Untar.swift */; }; 3466654F43654D36B5D478CA /* config.json in Resources */ = {isa = PBXBuildFile; fileRef = 2679C48F8CD642C68116DD24 /* config.json */; }; 3F123FD0EA9146FEBC864879 /* MMSans-Medium.otf in Resources */ = {isa = PBXBuildFile; fileRef = 4A64F1985EEA45C0B027E517 /* MMSans-Medium.otf */; }; 49D8E62C506F4A63889EEC7F /* branch.json in Resources */ = {isa = PBXBuildFile; fileRef = FE3C9A2458A1416290DEDAD4 /* branch.json */; }; 650F2B9D24DC5FF200C3B9C4 /* libRCTAesForked.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 650F2B9C24DC5FEC00C3B9C4 /* libRCTAesForked.a */; }; 654378B0243E2ADC00571B9C /* File.swift in Sources */ = {isa = PBXBuildFile; fileRef = 654378AF243E2ADC00571B9C /* File.swift */; }; - 7696E77F73B5ADD7EE8190E0 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7EEA32C976A46B991D55FD4 /* ExpoModulesProvider.swift */; }; 8C3986ED969040AEBC7A3856 /* MMPoly-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = F10E7EBF946A4F6D8E229143 /* MMPoly-Regular.otf */; }; 8DE564ACA9934796B5E7B1EB /* MMSans-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = F3C919D8F42C47389FF643E7 /* MMSans-Regular.otf */; }; 9D9E53F67A884FDEBE9A4D3C /* Geist-RegularItalic.otf in Resources */ = {isa = PBXBuildFile; fileRef = 978781C44CFB4434873EDB69 /* Geist-RegularItalic.otf */; }; A1987088D4835E5FCCABC418 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 683865D794CE6007E46CAD3A /* ExpoModulesProvider.swift */; }; A1F5C74197AA4BB4B2F9C001 /* Geist-SemiBold.otf in Resources */ = {isa = PBXBuildFile; fileRef = A1F5C74197AA4BB4B2F9C011 /* Geist-SemiBold.otf */; }; A1F5C74197AA4BB4B2F9C002 /* Geist-SemiBoldItalic.otf in Resources */ = {isa = PBXBuildFile; fileRef = A1F5C74197AA4BB4B2F9C012 /* Geist-SemiBoldItalic.otf */; }; - A9A253A9A4C55258DD932254 /* libPods-MetaMask-QA.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B6C7C9864634E61C13A07C28 /* libPods-MetaMask-QA.a */; }; A9AB7F6A09E06325C0A71FA4 /* libPods-MetaMask-Flask.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F02EB68A6ACEF113F4693A8 /* libPods-MetaMask-Flask.a */; }; AA11BB22CC33DD44EE55FF66 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA11BB22CC33DD44EE55FF67 /* SplashScreen.storyboard */; }; AA11BB22CC33DD44EE55FF68 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA11BB22CC33DD44EE55FF67 /* SplashScreen.storyboard */; }; - AA11BB22CC33DD44EE55FF69 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA11BB22CC33DD44EE55FF67 /* SplashScreen.storyboard */; }; B0EF7FA927BD16EA00D48B4E /* ThemeColors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B0EF7FA827BD16EA00D48B4E /* ThemeColors.xcassets */; }; - B339FF03289ABD70001B89FB /* File.swift in Sources */ = {isa = PBXBuildFile; fileRef = 654378AF243E2ADC00571B9C /* File.swift */; }; - B339FF07289ABD70001B89FB /* LinkPresentation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F961A36A28105CF9007442B5 /* LinkPresentation.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; - B339FF08289ABD70001B89FB /* libRCTAesForked.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 650F2B9C24DC5FEC00C3B9C4 /* libRCTAesForked.a */; }; - B339FF09289ABD70001B89FB /* JavaScriptCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 153C1A742217BCDC0088EFE0 /* JavaScriptCore.framework */; }; - B339FF0C289ABD70001B89FB /* Branch.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 153F84C92319B8DB00C19B63 /* Branch.framework */; }; - B339FF10289ABD70001B89FB /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; - B339FF11289ABD70001B89FB /* InpageBridgeWeb3.js in Resources */ = {isa = PBXBuildFile; fileRef = 158B0639211A72F500DF3C74 /* InpageBridgeWeb3.js */; }; - B339FF12289ABD70001B89FB /* Metamask.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 15D158EC210BD8C8006982B5 /* Metamask.ttf */; }; - B339FF14289ABD70001B89FB /* ThemeColors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B0EF7FA827BD16EA00D48B4E /* ThemeColors.xcassets */; }; - B339FF17289ABD70001B89FB /* debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 15FDD82721B7642B006B7C35 /* debug.xcconfig */; }; - B339FF1C289ABD70001B89FB /* release.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 15FDD86021B76461006B7C35 /* release.xcconfig */; }; - B339FF23289ABD70001B89FB /* branch.json in Resources */ = {isa = PBXBuildFile; fileRef = FE3C9A2458A1416290DEDAD4 /* branch.json */; }; - B339FF32289ABD70001B89FB /* Branch.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 153F84C92319B8DB00C19B63 /* Branch.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - B339FF3C289ABF2C001B89FB /* MetaMask-QA-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = B339FEA72899852C001B89FB /* MetaMask-QA-Info.plist */; }; BAB8A7C7328F48B6AC38DCE9 /* Geist-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = B848D40B87744D32949BDC25 /* Geist-Regular.otf */; }; C7B6D2EC4EBB469F9E0658BE /* MMSans-Bold.otf in Resources */ = {isa = PBXBuildFile; fileRef = D3350113F0764105B1E60002 /* MMSans-Bold.otf */; }; - C8424AE42CCC01F900F0BEB7 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = C8424AE32CCC01F900F0BEB7 /* GoogleService-Info.plist */; }; C8424AE52CCC01F900F0BEB7 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = C8424AE32CCC01F900F0BEB7 /* GoogleService-Info.plist */; }; C8424AE62CCC01F900F0BEB7 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = C8424AE32CCC01F900F0BEB7 /* GoogleService-Info.plist */; }; CF9895772A3B49BE00B4C9B5 /* RCTMinimizer.m in Sources */ = {isa = PBXBuildFile; fileRef = CF9895762A3B49BE00B4C9B5 /* RCTMinimizer.m */; }; - CF9895782A3B49BE00B4C9B5 /* RCTMinimizer.m in Sources */ = {isa = PBXBuildFile; fileRef = CF9895762A3B49BE00B4C9B5 /* RCTMinimizer.m */; }; CF98DA9C28D9FEB700096782 /* RCTScreenshotDetect.m in Sources */ = {isa = PBXBuildFile; fileRef = CF98DA9B28D9FEB700096782 /* RCTScreenshotDetect.m */; }; - CFD8DFC828EDD4C800CC75F6 /* RCTScreenshotDetect.m in Sources */ = {isa = PBXBuildFile; fileRef = CF98DA9B28D9FEB700096782 /* RCTScreenshotDetect.m */; }; E4B580722E32F462008165E1 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = E4B580712E32F462008165E1 /* Expo.plist */; }; - E4B580732E32F462008165E1 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = E4B580712E32F462008165E1 /* Expo.plist */; }; E4B580742E32F462008165E1 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = E4B580712E32F462008165E1 /* Expo.plist */; }; E4B580762E33A001008165E1 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B580752E33A001008165E1 /* AppDelegate.swift */; }; E4B580772E33A001008165E1 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B580752E33A001008165E1 /* AppDelegate.swift */; }; - E4B580782E33A001008165E1 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B580752E33A001008165E1 /* AppDelegate.swift */; }; E83DB5522BBDF2AA00536063 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = E83DB5392BBDB14700536063 /* PrivacyInfo.xcprivacy */; }; - E83DB5532BBDF2AE00536063 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = E83DB5392BBDB14700536063 /* PrivacyInfo.xcprivacy */; }; E83DB5542BBDF2AF00536063 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = E83DB5392BBDB14700536063 /* PrivacyInfo.xcprivacy */; }; ED2E8FE6D71BE9319F3B27D3 /* libPods-MetaMask.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D2632307C64595BE1B8ABEAF /* libPods-MetaMask.a */; }; F0B2A3E101000001000000A1 /* BrazeHelper.mm in Sources */ = {isa = PBXBuildFile; fileRef = F0B2A3E101000001000000A0 /* BrazeHelper.mm */; }; F0B2A3E101000001000000A2 /* BrazeHelper.mm in Sources */ = {isa = PBXBuildFile; fileRef = F0B2A3E101000001000000A0 /* BrazeHelper.mm */; }; - F0B2A3E101000001000000A3 /* BrazeHelper.mm in Sources */ = {isa = PBXBuildFile; fileRef = F0B2A3E101000001000000A0 /* BrazeHelper.mm */; }; F23972D16903249A8EC120BD /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EB256CB3A1A7A1D942A95F6 /* ExpoModulesProvider.swift */; }; F961A37228105CF9007442B5 /* LinkPresentation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F961A36A28105CF9007442B5 /* LinkPresentation.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; /* End PBXBuildFile section */ @@ -128,13 +101,6 @@ remoteGlobalIDString = 32D980DD1BE9F11C00FA27E5; remoteInfo = RCTAesForked; }; - B339FEFE289ABD70001B89FB /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 153F84C42319B8DA00C19B63 /* BranchSDK.xcodeproj */; - proxyType = 1; - remoteGlobalIDString = E298D0511C73D1B800589D22; - remoteInfo = Branch; - }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -160,17 +126,6 @@ name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; - B339FF30289ABD70001B89FB /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - B339FF32289ABD70001B89FB /* Branch.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ @@ -204,7 +159,6 @@ 4A64F1985EEA45C0B027E517 /* MMSans-Medium.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "MMSans-Medium.otf"; path = "../app/fonts/MMSans-Medium.otf"; sourceTree = ""; }; 4BFDB3B860044F1A9CF3CFEB /* Geist-Medium.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Geist-Medium.otf"; path = "../app/fonts/Geist-Medium.otf"; sourceTree = ""; }; 4C81CC9BCD86AC7F96BA8CAD /* Pods-MetaMask.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MetaMask.debug.xcconfig"; path = "Target Support Files/Pods-MetaMask/Pods-MetaMask.debug.xcconfig"; sourceTree = ""; }; - 51AB7231D0E692F5EF71FACB /* Pods-MetaMask-QA.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MetaMask-QA.debug.xcconfig"; path = "Target Support Files/Pods-MetaMask-QA/Pods-MetaMask-QA.debug.xcconfig"; sourceTree = ""; }; 57C103F40F394637B5A886FC /* FontAwesome5_Brands.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = FontAwesome5_Brands.ttf; path = "../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Brands.ttf"; sourceTree = ""; }; 5E32A09A7BDC431FA403BA73 /* FontAwesome.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = FontAwesome.ttf; path = "../node_modules/react-native-vector-icons/Fonts/FontAwesome.ttf"; sourceTree = ""; }; 650F2B9724DC5FEB00C3B9C4 /* RCTAesForked.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTAesForked.xcodeproj; path = "../node_modules/react-native-aes-crypto-forked/ios/RCTAesForked.xcodeproj"; sourceTree = ""; }; @@ -226,13 +180,9 @@ AA11BB22CC33DD44EE55FF67 /* SplashScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = MetaMask/SplashScreen.storyboard; sourceTree = ""; }; AA9EDF17249955C7005D89EE /* MetaMaskDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = MetaMaskDebug.entitlements; path = MetaMask/MetaMaskDebug.entitlements; sourceTree = ""; }; B0EF7FA827BD16EA00D48B4E /* ThemeColors.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = ThemeColors.xcassets; sourceTree = ""; }; - B339FEA72899852C001B89FB /* MetaMask-QA-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "MetaMask-QA-Info.plist"; path = "MetaMask/MetaMask-QA-Info.plist"; sourceTree = ""; }; - B339FF39289ABD70001B89FB /* MetaMask-QA.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "MetaMask-QA.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - B6C7C9864634E61C13A07C28 /* libPods-MetaMask-QA.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-MetaMask-QA.a"; sourceTree = BUILT_PRODUCTS_DIR; }; B848D40B87744D32949BDC25 /* Geist-Regular.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Geist-Regular.otf"; path = "../app/fonts/Geist-Regular.otf"; sourceTree = ""; }; BF485CDA047B4D52852B87F5 /* EvilIcons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = EvilIcons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/EvilIcons.ttf"; sourceTree = ""; }; C8424AE32CCC01F900F0BEB7 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; - CF014205BB8964CFE74D4D8E /* Pods-MetaMask-QA.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MetaMask-QA.release.xcconfig"; path = "Target Support Files/Pods-MetaMask-QA/Pods-MetaMask-QA.release.xcconfig"; sourceTree = ""; }; CF9895752A3B48F700B4C9B5 /* RCTMinimizer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RCTMinimizer.h; path = MetaMask/NativeModules/RCTMinimizer/RCTMinimizer.h; sourceTree = ""; }; CF9895762A3B49BE00B4C9B5 /* RCTMinimizer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = RCTMinimizer.m; path = MetaMask/NativeModules/RCTMinimizer/RCTMinimizer.m; sourceTree = ""; }; CF98DA9A28D9FE7800096782 /* RCTScreenshotDetect.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTScreenshotDetect.h; sourceTree = ""; }; @@ -242,7 +192,6 @@ D3350113F0764105B1E60002 /* MMSans-Bold.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "MMSans-Bold.otf"; path = "../app/fonts/MMSans-Bold.otf"; sourceTree = ""; }; E4B580712E32F462008165E1 /* Expo.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = ""; }; E4B580752E33A001008165E1 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetaMask/AppDelegate.swift; sourceTree = ""; }; - E7EEA32C976A46B991D55FD4 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-MetaMask-QA/ExpoModulesProvider.swift"; sourceTree = ""; }; E83DB5392BBDB14700536063 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = MetaMask/PrivacyInfo.xcprivacy; sourceTree = SOURCE_ROOT; }; E9629905BA1940ADA4189921 /* Feather.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Feather.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Feather.ttf"; sourceTree = ""; }; EBC2B6371CD846D28B9FAADF /* FontAwesome5_Regular.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = FontAwesome5_Regular.ttf; path = "../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Regular.ttf"; sourceTree = ""; }; @@ -281,18 +230,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - B339FF06289ABD70001B89FB /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - B339FF07289ABD70001B89FB /* LinkPresentation.framework in Frameworks */, - B339FF08289ABD70001B89FB /* libRCTAesForked.a in Frameworks */, - B339FF09289ABD70001B89FB /* JavaScriptCore.framework in Frameworks */, - B339FF0C289ABD70001B89FB /* Branch.framework in Frameworks */, - A9A253A9A4C55258DD932254 /* libPods-MetaMask-QA.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -326,7 +263,6 @@ children = ( E4B580712E32F462008165E1 /* Expo.plist */, E83DB5392BBDB14700536063 /* PrivacyInfo.xcprivacy */, - B339FEA72899852C001B89FB /* MetaMask-QA-Info.plist */, AA9EDF17249955C7005D89EE /* MetaMaskDebug.entitlements */, 15F7796222A1BC1E00B1DF8C /* NativeModules */, 15205D6221596AD90049EA93 /* MetaMask.entitlements */, @@ -387,7 +323,6 @@ 2D16E6891FA4F8E400B85C8A /* libReact.a */, D2632307C64595BE1B8ABEAF /* libPods-MetaMask.a */, 9F02EB68A6ACEF113F4693A8 /* libPods-MetaMask-Flask.a */, - B6C7C9864634E61C13A07C28 /* libPods-MetaMask-QA.a */, ); name = Frameworks; sourceTree = ""; @@ -481,7 +416,6 @@ isa = PBXGroup; children = ( 13B07F961A680F5B00A75B9A /* MetaMask.app */, - B339FF39289ABD70001B89FB /* MetaMask-QA.app */, 2EF282922B0FF86900D7B4B1 /* MetaMask-Flask.app */, ); name = Products; @@ -494,8 +428,6 @@ 7D2A2666F9BADDF2418B01A1 /* Pods-MetaMask.release.xcconfig */, 91B348F39D8AD3220320E89D /* Pods-MetaMask-Flask.debug.xcconfig */, F1CCBB0591B4D16C1710A05D /* Pods-MetaMask-Flask.release.xcconfig */, - 51AB7231D0E692F5EF71FACB /* Pods-MetaMask-QA.debug.xcconfig */, - CF014205BB8964CFE74D4D8E /* Pods-MetaMask-QA.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -505,7 +437,6 @@ children = ( 299C51B8AA60DA51C494DE7A /* MetaMask */, 089A67E20C8950FFA11688EA /* MetaMask-Flask */, - D48FD973918C14EFC848CBFB /* MetaMask-QA */, ); name = ExpoModulesProviders; sourceTree = ""; @@ -528,14 +459,6 @@ name = RCTScreenshotDetect; sourceTree = ""; }; - D48FD973918C14EFC848CBFB /* MetaMask-QA */ = { - isa = PBXGroup; - children = ( - E7EEA32C976A46B991D55FD4 /* ExpoModulesProvider.swift */, - ); - name = "MetaMask-QA"; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -593,33 +516,6 @@ productReference = 2EF282922B0FF86900D7B4B1 /* MetaMask-Flask.app */; productType = "com.apple.product-type.application"; }; - B339FEF8289ABD70001B89FB /* MetaMask-QA */ = { - isa = PBXNativeTarget; - buildConfigurationList = B339FF36289ABD70001B89FB /* Build configuration list for PBXNativeTarget "MetaMask-QA" */; - buildPhases = ( - 7F95098E1DEEA467CD6B2B8B /* [CP] Check Pods Manifest.lock */, - B339FF00289ABD70001B89FB /* Override xcconfig files */, - 056B914267B20A3E1A9AEF1A /* [Expo] Configure project */, - B339FF01289ABD70001B89FB /* Sources */, - B339FF06289ABD70001B89FB /* Frameworks */, - B339FF0F289ABD70001B89FB /* Resources */, - B339FF30289ABD70001B89FB /* Embed Frameworks */, - B339FF2F289ABD70001B89FB /* Bundle JS Code & Upload Sentry Files */, - C809907F60335F19DA480743 /* [CP] Embed Pods Frameworks */, - 475B37D211D24FD533A25DD4 /* [CP] Copy Pods Resources */, - 13E0EBB030DB9498ACF206AC /* [CP-User] [RNFB] Core Configuration */, - B04B18D62D8B34AD00C5C2CE /* Strip Bitcode */, - ); - buildRules = ( - ); - dependencies = ( - B339FEFD289ABD70001B89FB /* PBXTargetDependency */, - ); - name = "MetaMask-QA"; - productName = "Hello World"; - productReference = B339FF39289ABD70001B89FB /* MetaMask-QA.app */; - productType = "com.apple.product-type.application"; - }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -667,7 +563,6 @@ projectRoot = ""; targets = ( 13B07F861A680F5B00A75B9A /* MetaMask */, - B339FEF8289ABD70001B89FB /* MetaMask-QA */, 2EF282522B0FF86900D7B4B1 /* MetaMask-Flask */, ); }; @@ -738,25 +633,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - B339FF0F289ABD70001B89FB /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - E4B580732E32F462008165E1 /* Expo.plist in Resources */, - B339FF10289ABD70001B89FB /* Images.xcassets in Resources */, - B339FF11289ABD70001B89FB /* InpageBridgeWeb3.js in Resources */, - B339FF12289ABD70001B89FB /* Metamask.ttf in Resources */, - B339FF14289ABD70001B89FB /* ThemeColors.xcassets in Resources */, - B339FF17289ABD70001B89FB /* debug.xcconfig in Resources */, - C8424AE42CCC01F900F0BEB7 /* GoogleService-Info.plist in Resources */, - E83DB5532BBDF2AE00536063 /* PrivacyInfo.xcprivacy in Resources */, - B339FF1C289ABD70001B89FB /* release.xcconfig in Resources */, - AA11BB22CC33DD44EE55FF69 /* SplashScreen.storyboard in Resources */, - B339FF23289ABD70001B89FB /* branch.json in Resources */, - B339FF3C289ABF2C001B89FB /* MetaMask-QA-Info.plist in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -777,30 +653,6 @@ shellPath = /bin/sh; shellScript = "# Define script\nBUNDLE_AND_UPLOAD_TO_SENTRY=\"../scripts/ios/bundle-js-and-sentry-upload.sh\"\n\n# Give permissions to script\nchmod +x $BUNDLE_AND_UPLOAD_TO_SENTRY\n\n# Run script\n$BUNDLE_AND_UPLOAD_TO_SENTRY\n"; }; - 056B914267B20A3E1A9AEF1A /* [Expo] Configure project */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "$(SRCROOT)/.xcode.env", - "$(SRCROOT)/.xcode.env.local", - "$(SRCROOT)/MetaMask/MetaMaskDebug.entitlements", - "$(SRCROOT)/Pods/Target Support Files/Pods-MetaMask-QA/expo-configure-project.sh", - ); - name = "[Expo] Configure project"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(SRCROOT)/Pods/Target Support Files/Pods-MetaMask-QA/ExpoModulesProvider.swift", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-MetaMask-QA/expo-configure-project.sh\"\n"; - }; 1315792FDF9ED5C1277541D0 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -818,19 +670,6 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-MetaMask/Pods-MetaMask-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - 13E0EBB030DB9498ACF206AC /* [CP-User] [RNFB] Core Configuration */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)", - ); - name = "[CP-User] [RNFB] Core Configuration"; - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "#!/usr/bin/env bash\n#\n# Copyright (c) 2016-present Invertase Limited & Contributors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this library except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##########################################################################\n##########################################################################\n#\n# NOTE THAT IF YOU CHANGE THIS FILE YOU MUST RUN pod install AFTERWARDS\n#\n# This file is installed as an Xcode build script in the project file\n# by cocoapods, and you will not see your changes until you pod install\n#\n##########################################################################\n##########################################################################\n\nset -e\n\n_MAX_LOOKUPS=2;\n_SEARCH_RESULT=''\n_RN_ROOT_EXISTS=''\n_CURRENT_LOOKUPS=1\n_JSON_ROOT=\"'react-native'\"\n_JSON_FILE_NAME='firebase.json'\n_JSON_OUTPUT_BASE64='e30=' # { }\n_CURRENT_SEARCH_DIR=${PROJECT_DIR}\n_PLIST_BUDDY=/usr/libexec/PlistBuddy\n_TARGET_PLIST=\"${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH}\"\n_DSYM_PLIST=\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist\"\n\n# plist arrays\n_PLIST_ENTRY_KEYS=()\n_PLIST_ENTRY_TYPES=()\n_PLIST_ENTRY_VALUES=()\n\nfunction setPlistValue {\n echo \"info: setting plist entry '$1' of type '$2' in file '$4'\"\n ${_PLIST_BUDDY} -c \"Add :$1 $2 '$3'\" $4 || echo \"info: '$1' already exists\"\n}\n\nfunction getFirebaseJsonKeyValue () {\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n ruby -Ku -e \"require 'rubygems';require 'json'; output=JSON.parse('$1'); puts output[$_JSON_ROOT]['$2']\"\n else\n echo \"\"\n fi;\n}\n\nfunction jsonBoolToYesNo () {\n if [[ $1 == \"false\" ]]; then\n echo \"NO\"\n elif [[ $1 == \"true\" ]]; then\n echo \"YES\"\n else echo \"NO\"\n fi\n}\n\necho \"info: -> RNFB build script started\"\necho \"info: 1) Locating ${_JSON_FILE_NAME} file:\"\n\nif [[ -z ${_CURRENT_SEARCH_DIR} ]]; then\n _CURRENT_SEARCH_DIR=$(pwd)\nfi;\n\nwhile true; do\n _CURRENT_SEARCH_DIR=$(dirname \"$_CURRENT_SEARCH_DIR\")\n if [[ \"$_CURRENT_SEARCH_DIR\" == \"/\" ]] || [[ ${_CURRENT_LOOKUPS} -gt ${_MAX_LOOKUPS} ]]; then break; fi;\n echo \"info: ($_CURRENT_LOOKUPS of $_MAX_LOOKUPS) Searching in '$_CURRENT_SEARCH_DIR' for a ${_JSON_FILE_NAME} file.\"\n _SEARCH_RESULT=$(find \"$_CURRENT_SEARCH_DIR\" -maxdepth 2 -name ${_JSON_FILE_NAME} -print | /usr/bin/head -n 1)\n if [[ ${_SEARCH_RESULT} ]]; then\n echo \"info: ${_JSON_FILE_NAME} found at $_SEARCH_RESULT\"\n break;\n fi;\n _CURRENT_LOOKUPS=$((_CURRENT_LOOKUPS+1))\ndone\n\nif [[ ${_SEARCH_RESULT} ]]; then\n _JSON_OUTPUT_RAW=$(cat \"${_SEARCH_RESULT}\")\n _RN_ROOT_EXISTS=$(ruby -Ku -e \"require 'rubygems';require 'json'; output=JSON.parse('$_JSON_OUTPUT_RAW'); puts output[$_JSON_ROOT]\" || echo '')\n\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n if ! python3 --version >/dev/null 2>&1; then echo \"python3 not found, firebase.json file processing error.\" && exit 1; fi\n _JSON_OUTPUT_BASE64=$(python3 -c 'import json,sys,base64;print(base64.b64encode(bytes(json.dumps(json.loads(open('\"'${_SEARCH_RESULT}'\"', '\"'rb'\"').read())['${_JSON_ROOT}']), '\"'utf-8'\"')).decode())' || echo \"e30=\")\n fi\n\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n\n # config.app_data_collection_default_enabled\n _APP_DATA_COLLECTION_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_data_collection_default_enabled\")\n if [[ $_APP_DATA_COLLECTION_ENABLED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseDataCollectionDefaultEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_DATA_COLLECTION_ENABLED\")\")\n fi\n\n # config.analytics_auto_collection_enabled\n _ANALYTICS_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_auto_collection_enabled\")\n if [[ $_ANALYTICS_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AUTO_COLLECTION\")\")\n fi\n\n # config.analytics_collection_deactivated\n _ANALYTICS_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_collection_deactivated\")\n if [[ $_ANALYTICS_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_DEACTIVATED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_DEACTIVATED\")\")\n fi\n\n # config.analytics_idfv_collection_enabled\n _ANALYTICS_IDFV_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_idfv_collection_enabled\")\n if [[ $_ANALYTICS_IDFV_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_IDFV_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_IDFV_COLLECTION\")\")\n fi\n\n # config.analytics_default_allow_analytics_storage\n _ANALYTICS_STORAGE=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_analytics_storage\")\n if [[ $_ANALYTICS_STORAGE ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_ANALYTICS_STORAGE\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_STORAGE\")\")\n fi\n\n # config.analytics_default_allow_ad_storage\n _ANALYTICS_AD_STORAGE=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_storage\")\n if [[ $_ANALYTICS_AD_STORAGE ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_STORAGE\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AD_STORAGE\")\")\n fi\n\n # config.analytics_default_allow_ad_user_data\n _ANALYTICS_AD_USER_DATA=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_user_data\")\n if [[ $_ANALYTICS_AD_USER_DATA ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_USER_DATA\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AD_USER_DATA\")\")\n fi\n\n # config.analytics_default_allow_ad_personalization_signals\n _ANALYTICS_PERSONALIZATION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_personalization_signals\")\n if [[ $_ANALYTICS_PERSONALIZATION ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_PERSONALIZATION_SIGNALS\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_PERSONALIZATION\")\")\n fi\n\n # config.analytics_registration_with_ad_network_enabled\n _ANALYTICS_REGISTRATION_WITH_AD_NETWORK=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"google_analytics_registration_with_ad_network_enabled\")\n if [[ $_ANALYTICS_REGISTRATION_WITH_AD_NETWORK ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_REGISTRATION_WITH_AD_NETWORK_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_REGISTRATION_WITH_AD_NETWORK\")\")\n fi\n\n # config.google_analytics_automatic_screen_reporting_enabled\n _ANALYTICS_AUTO_SCREEN_REPORTING=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"google_analytics_automatic_screen_reporting_enabled\")\n if [[ $_ANALYTICS_AUTO_SCREEN_REPORTING ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseAutomaticScreenReportingEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AUTO_SCREEN_REPORTING\")\")\n fi\n\n # config.perf_auto_collection_enabled\n _PERF_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"perf_auto_collection_enabled\")\n if [[ $_PERF_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"firebase_performance_collection_enabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_PERF_AUTO_COLLECTION\")\")\n fi\n\n # config.perf_collection_deactivated\n _PERF_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"perf_collection_deactivated\")\n if [[ $_PERF_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"firebase_performance_collection_deactivated\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_PERF_DEACTIVATED\")\")\n fi\n\n # config.messaging_auto_init_enabled\n _MESSAGING_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"messaging_auto_init_enabled\")\n if [[ $_MESSAGING_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseMessagingAutoInitEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_MESSAGING_AUTO_INIT\")\")\n fi\n\n # config.in_app_messaging_auto_colllection_enabled\n _FIAM_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"in_app_messaging_auto_collection_enabled\")\n if [[ $_FIAM_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseInAppMessagingAutomaticDataCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_FIAM_AUTO_INIT\")\")\n fi\n\n # config.app_check_token_auto_refresh\n _APP_CHECK_TOKEN_AUTO_REFRESH=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_check_token_auto_refresh\")\n if [[ $_APP_CHECK_TOKEN_AUTO_REFRESH ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseAppCheckTokenAutoRefreshEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_CHECK_TOKEN_AUTO_REFRESH\")\")\n fi\n\n # config.crashlytics_disable_auto_disabler - undocumented for now - mainly for debugging, document if becomes useful\n _CRASHLYTICS_AUTO_DISABLE_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"crashlytics_disable_auto_disabler\")\n if [[ $_CRASHLYTICS_AUTO_DISABLE_ENABLED == \"true\" ]]; then\n echo \"Disabled Crashlytics auto disabler.\" # do nothing\n else\n _PLIST_ENTRY_KEYS+=(\"FirebaseCrashlyticsCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"NO\")\n fi\nelse\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n echo \"warning: A firebase.json file was not found, whilst this file is optional it is recommended to include it to configure firebase services in React Native Firebase.\"\nfi;\n\necho \"info: 2) Injecting Info.plist entries: \"\n\n# Log out the keys we're adding\nfor i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n echo \" -> $i) ${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\"\ndone\n\nfor plist in \"${_TARGET_PLIST}\" \"${_DSYM_PLIST}\" ; do\n if [[ -f \"${plist}\" ]]; then\n\n # paths with spaces break the call to setPlistValue. temporarily modify\n # the shell internal field separator variable (IFS), which normally\n # includes spaces, to consist only of line breaks\n oldifs=$IFS\n IFS=\"\n\"\n\n for i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n setPlistValue \"${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\" \"${plist}\"\n done\n\n # restore the original internal field separator value\n IFS=$oldifs\n else\n echo \"warning: A Info.plist build output file was not found (${plist})\"\n fi\ndone\n\necho \"info: <- RNFB build script finished\"\n"; - }; 15FDD86321B76696006B7C35 /* Override xcconfig files */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -947,23 +786,6 @@ shellPath = /bin/sh; shellScript = "# Define script\nBUNDLE_AND_UPLOAD_TO_SENTRY=\"../scripts/ios/bundle-js-and-sentry-upload.sh\"\n\n# Give permissions to script\nchmod +x $BUNDLE_AND_UPLOAD_TO_SENTRY\n\n# Run script\n$BUNDLE_AND_UPLOAD_TO_SENTRY\n"; }; - 475B37D211D24FD533A25DD4 /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-MetaMask-QA/Pods-MetaMask-QA-resources-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Copy Pods Resources"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-MetaMask-QA/Pods-MetaMask-QA-resources-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-MetaMask-QA/Pods-MetaMask-QA-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; 65728037EE7BD20DE039438B /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -999,28 +821,6 @@ shellPath = /bin/sh; shellScript = "#!/usr/bin/env bash\n#\n# Copyright (c) 2016-present Invertase Limited & Contributors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this library except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##########################################################################\n##########################################################################\n#\n# NOTE THAT IF YOU CHANGE THIS FILE YOU MUST RUN pod install AFTERWARDS\n#\n# This file is installed as an Xcode build script in the project file\n# by cocoapods, and you will not see your changes until you pod install\n#\n##########################################################################\n##########################################################################\n\nset -e\n\n_MAX_LOOKUPS=2;\n_SEARCH_RESULT=''\n_RN_ROOT_EXISTS=''\n_CURRENT_LOOKUPS=1\n_JSON_ROOT=\"'react-native'\"\n_JSON_FILE_NAME='firebase.json'\n_JSON_OUTPUT_BASE64='e30=' # { }\n_CURRENT_SEARCH_DIR=${PROJECT_DIR}\n_PLIST_BUDDY=/usr/libexec/PlistBuddy\n_TARGET_PLIST=\"${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH}\"\n_DSYM_PLIST=\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist\"\n\n# plist arrays\n_PLIST_ENTRY_KEYS=()\n_PLIST_ENTRY_TYPES=()\n_PLIST_ENTRY_VALUES=()\n\nfunction setPlistValue {\n echo \"info: setting plist entry '$1' of type '$2' in file '$4'\"\n ${_PLIST_BUDDY} -c \"Add :$1 $2 '$3'\" $4 || echo \"info: '$1' already exists\"\n}\n\nfunction getFirebaseJsonKeyValue () {\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n ruby -Ku -e \"require 'rubygems';require 'json'; output=JSON.parse('$1'); puts output[$_JSON_ROOT]['$2']\"\n else\n echo \"\"\n fi;\n}\n\nfunction jsonBoolToYesNo () {\n if [[ $1 == \"false\" ]]; then\n echo \"NO\"\n elif [[ $1 == \"true\" ]]; then\n echo \"YES\"\n else echo \"NO\"\n fi\n}\n\necho \"info: -> RNFB build script started\"\necho \"info: 1) Locating ${_JSON_FILE_NAME} file:\"\n\nif [[ -z ${_CURRENT_SEARCH_DIR} ]]; then\n _CURRENT_SEARCH_DIR=$(pwd)\nfi;\n\nwhile true; do\n _CURRENT_SEARCH_DIR=$(dirname \"$_CURRENT_SEARCH_DIR\")\n if [[ \"$_CURRENT_SEARCH_DIR\" == \"/\" ]] || [[ ${_CURRENT_LOOKUPS} -gt ${_MAX_LOOKUPS} ]]; then break; fi;\n echo \"info: ($_CURRENT_LOOKUPS of $_MAX_LOOKUPS) Searching in '$_CURRENT_SEARCH_DIR' for a ${_JSON_FILE_NAME} file.\"\n _SEARCH_RESULT=$(find \"$_CURRENT_SEARCH_DIR\" -maxdepth 2 -name ${_JSON_FILE_NAME} -print | /usr/bin/head -n 1)\n if [[ ${_SEARCH_RESULT} ]]; then\n echo \"info: ${_JSON_FILE_NAME} found at $_SEARCH_RESULT\"\n break;\n fi;\n _CURRENT_LOOKUPS=$((_CURRENT_LOOKUPS+1))\ndone\n\nif [[ ${_SEARCH_RESULT} ]]; then\n _JSON_OUTPUT_RAW=$(cat \"${_SEARCH_RESULT}\")\n _RN_ROOT_EXISTS=$(ruby -Ku -e \"require 'rubygems';require 'json'; output=JSON.parse('$_JSON_OUTPUT_RAW'); puts output[$_JSON_ROOT]\" || echo '')\n\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n if ! python3 --version >/dev/null 2>&1; then echo \"python3 not found, firebase.json file processing error.\" && exit 1; fi\n _JSON_OUTPUT_BASE64=$(python3 -c 'import json,sys,base64;print(base64.b64encode(bytes(json.dumps(json.loads(open('\"'${_SEARCH_RESULT}'\"', '\"'rb'\"').read())['${_JSON_ROOT}']), '\"'utf-8'\"')).decode())' || echo \"e30=\")\n fi\n\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n\n # config.app_data_collection_default_enabled\n _APP_DATA_COLLECTION_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_data_collection_default_enabled\")\n if [[ $_APP_DATA_COLLECTION_ENABLED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseDataCollectionDefaultEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_DATA_COLLECTION_ENABLED\")\")\n fi\n\n # config.analytics_auto_collection_enabled\n _ANALYTICS_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_auto_collection_enabled\")\n if [[ $_ANALYTICS_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AUTO_COLLECTION\")\")\n fi\n\n # config.analytics_collection_deactivated\n _ANALYTICS_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_collection_deactivated\")\n if [[ $_ANALYTICS_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_DEACTIVATED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_DEACTIVATED\")\")\n fi\n\n # config.analytics_idfv_collection_enabled\n _ANALYTICS_IDFV_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_idfv_collection_enabled\")\n if [[ $_ANALYTICS_IDFV_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_IDFV_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_IDFV_COLLECTION\")\")\n fi\n\n # config.analytics_default_allow_analytics_storage\n _ANALYTICS_STORAGE=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_analytics_storage\")\n if [[ $_ANALYTICS_STORAGE ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_ANALYTICS_STORAGE\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_STORAGE\")\")\n fi\n\n # config.analytics_default_allow_ad_storage\n _ANALYTICS_AD_STORAGE=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_storage\")\n if [[ $_ANALYTICS_AD_STORAGE ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_STORAGE\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AD_STORAGE\")\")\n fi\n\n # config.analytics_default_allow_ad_user_data\n _ANALYTICS_AD_USER_DATA=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_user_data\")\n if [[ $_ANALYTICS_AD_USER_DATA ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_USER_DATA\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AD_USER_DATA\")\")\n fi\n\n # config.analytics_default_allow_ad_personalization_signals\n _ANALYTICS_PERSONALIZATION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_personalization_signals\")\n if [[ $_ANALYTICS_PERSONALIZATION ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_PERSONALIZATION_SIGNALS\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_PERSONALIZATION\")\")\n fi\n\n # config.analytics_registration_with_ad_network_enabled\n _ANALYTICS_REGISTRATION_WITH_AD_NETWORK=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"google_analytics_registration_with_ad_network_enabled\")\n if [[ $_ANALYTICS_REGISTRATION_WITH_AD_NETWORK ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_REGISTRATION_WITH_AD_NETWORK_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_REGISTRATION_WITH_AD_NETWORK\")\")\n fi\n\n # config.google_analytics_automatic_screen_reporting_enabled\n _ANALYTICS_AUTO_SCREEN_REPORTING=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"google_analytics_automatic_screen_reporting_enabled\")\n if [[ $_ANALYTICS_AUTO_SCREEN_REPORTING ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseAutomaticScreenReportingEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AUTO_SCREEN_REPORTING\")\")\n fi\n\n # config.perf_auto_collection_enabled\n _PERF_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"perf_auto_collection_enabled\")\n if [[ $_PERF_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"firebase_performance_collection_enabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_PERF_AUTO_COLLECTION\")\")\n fi\n\n # config.perf_collection_deactivated\n _PERF_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"perf_collection_deactivated\")\n if [[ $_PERF_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"firebase_performance_collection_deactivated\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_PERF_DEACTIVATED\")\")\n fi\n\n # config.messaging_auto_init_enabled\n _MESSAGING_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"messaging_auto_init_enabled\")\n if [[ $_MESSAGING_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseMessagingAutoInitEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_MESSAGING_AUTO_INIT\")\")\n fi\n\n # config.in_app_messaging_auto_colllection_enabled\n _FIAM_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"in_app_messaging_auto_collection_enabled\")\n if [[ $_FIAM_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseInAppMessagingAutomaticDataCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_FIAM_AUTO_INIT\")\")\n fi\n\n # config.app_check_token_auto_refresh\n _APP_CHECK_TOKEN_AUTO_REFRESH=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_check_token_auto_refresh\")\n if [[ $_APP_CHECK_TOKEN_AUTO_REFRESH ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseAppCheckTokenAutoRefreshEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_CHECK_TOKEN_AUTO_REFRESH\")\")\n fi\n\n # config.crashlytics_disable_auto_disabler - undocumented for now - mainly for debugging, document if becomes useful\n _CRASHLYTICS_AUTO_DISABLE_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"crashlytics_disable_auto_disabler\")\n if [[ $_CRASHLYTICS_AUTO_DISABLE_ENABLED == \"true\" ]]; then\n echo \"Disabled Crashlytics auto disabler.\" # do nothing\n else\n _PLIST_ENTRY_KEYS+=(\"FirebaseCrashlyticsCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"NO\")\n fi\nelse\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n echo \"warning: A firebase.json file was not found, whilst this file is optional it is recommended to include it to configure firebase services in React Native Firebase.\"\nfi;\n\necho \"info: 2) Injecting Info.plist entries: \"\n\n# Log out the keys we're adding\nfor i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n echo \" -> $i) ${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\"\ndone\n\nfor plist in \"${_TARGET_PLIST}\" \"${_DSYM_PLIST}\" ; do\n if [[ -f \"${plist}\" ]]; then\n\n # paths with spaces break the call to setPlistValue. temporarily modify\n # the shell internal field separator variable (IFS), which normally\n # includes spaces, to consist only of line breaks\n oldifs=$IFS\n IFS=\"\n\"\n\n for i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n setPlistValue \"${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\" \"${plist}\"\n done\n\n # restore the original internal field separator value\n IFS=$oldifs\n else\n echo \"warning: A Info.plist build output file was not found (${plist})\"\n fi\ndone\n\necho \"info: <- RNFB build script finished\"\n"; }; - 7F95098E1DEEA467CD6B2B8B /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-MetaMask-QA-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; 9F2FDF243A79F1A3A790828C /* [CP-User] [RNFB] Core Configuration */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -1076,24 +876,6 @@ shellPath = /bin/sh; shellScript = "# Script for stripping bitcode when building using Xcode 16+ and uploading to the App Store\n# Reference - https://discuss.bitrise.io/t/xcode-16-known-issues/24484\n# This script should be last to ensure that all bitcode is stripped after dependencies are installed\n\n\nif [ \"${CONFIGURATION}\" = \"Release\" ]; then\n find \"${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}\" -name 'OpenSSL' -exec xcrun bitcode_strip {} -r -o {} \\;\nfi\n"; }; - B04B18D62D8B34AD00C5C2CE /* Strip Bitcode */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = "Strip Bitcode"; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "# Script for stripping bitcode when building using Xcode 16+ and uploading to the App Store\n# Reference - https://discuss.bitrise.io/t/xcode-16-known-issues/24484\n# This script should be last to ensure that all bitcode is stripped after dependencies are installed\n\nfind \"${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}\" -name 'OpenSSL' -exec xcrun bitcode_strip {} -r -o {} \\;\n"; - }; B04B18D72D8B34BE00C5C2CE /* Strip Bitcode */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -1112,58 +894,6 @@ shellPath = /bin/sh; shellScript = "# Script for stripping bitcode when building using Xcode 16+ and uploading to the App Store\n# Reference - https://discuss.bitrise.io/t/xcode-16-known-issues/24484\n# This script should be last to ensure that all bitcode is stripped after dependencies are installed\n\nfind \"${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}\" -name 'OpenSSL' -exec xcrun bitcode_strip {} -r -o {} \\;\n"; }; - B339FF00289ABD70001B89FB /* Override xcconfig files */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = "Override xcconfig files"; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "if [ -e ../.ios.env ]\nthen\n cp -rf ../.ios.env debug.xcconfig\n cp -rf ../.ios.env release.xcconfig\nelse\n cp -rf ../.ios.env.example debug.xcconfig\n cp -rf ../.ios.env.example release.xcconfig\nfi\n\n"; - }; - B339FF2F289ABD70001B89FB /* Bundle JS Code & Upload Sentry Files */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-MetaMask/Pods-MetaMask-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - inputPaths = ( - ); - name = "Bundle JS Code & Upload Sentry Files"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "# Define script\nBUNDLE_AND_UPLOAD_TO_SENTRY=\"../scripts/ios/bundle-js-and-sentry-upload.sh\"\n\n# Give permissions to script\nchmod +x $BUNDLE_AND_UPLOAD_TO_SENTRY\n\n# Run script\n$BUNDLE_AND_UPLOAD_TO_SENTRY\n"; - }; - C809907F60335F19DA480743 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-MetaMask-QA/Pods-MetaMask-QA-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-MetaMask-QA/Pods-MetaMask-QA-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-MetaMask-QA/Pods-MetaMask-QA-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; E6DF8EB7C7F8301263C260CE /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -1233,22 +963,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - B339FF01289ABD70001B89FB /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - CFD8DFC828EDD4C800CC75F6 /* RCTScreenshotDetect.m in Sources */, - 2EF283332B17EC1A00D7B4B1 /* RNTar.m in Sources */, - E4B580782E33A001008165E1 /* AppDelegate.swift in Sources */, - F0B2A3E101000001000000A3 /* BrazeHelper.mm in Sources */, - 2EF2832B2B17EBD600D7B4B1 /* RnTar.swift in Sources */, - 2EF283382B17EC7900D7B4B1 /* Light-Swift-Untar.swift in Sources */, - B339FF03289ABD70001B89FB /* File.swift in Sources */, - CF9895782A3B49BE00B4C9B5 /* RCTMinimizer.m in Sources */, - 7696E77F73B5ADD7EE8190E0 /* ExpoModulesProvider.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -1262,11 +976,6 @@ name = Branch; targetProxy = 2EF282562B0FF86900D7B4B1 /* PBXContainerItemProxy */; }; - B339FEFD289ABD70001B89FB /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - name = Branch; - targetProxy = B339FEFE289ABD70001B89FB /* PBXContainerItemProxy */; - }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ @@ -1635,144 +1344,6 @@ }; name = Release; }; - B339FF37289ABD70001B89FB /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 51AB7231D0E692F5EF71FACB /* Pods-MetaMask-QA.debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-QA"; - ASSETCATALOG_COMPILER_OPTIMIZATION = time; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMaskDebug.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4823; - DEAD_CODE_STRIPPING = YES; - DEBUG_INFORMATION_FORMAT = dwarf; - DEVELOPMENT_TEAM = 48XVW22RCG; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)", - ); - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - HEADER_SEARCH_PATHS = ( - "$(inherited)", - "$(SRCROOT)/../node_modules/react-native-wkwebview-reborn/ios/RCTWKWebView", - "$(SRCROOT)/../node_modules/react-native-keychain/RNKeychainManager", - "$(SRCROOT)/../node_modules/react-native-share/ios", - "$(SRCROOT)/../node_modules/react-native-branch/ios/**", - "$(SRCROOT)/../node_modules/@metamask/react-native-search-api/ios/RCTSearchApi", - "$(SRCROOT)/../node_modules/lottie-ios/lottie-ios/Classes/**", - "$(SRCROOT)/../node_modules/react-native-view-shot/ios", - "$(SRCROOT)/../node_modules/react-native-tcp/ios/**", - ); - INFOPLIST_FILE = "MetaMask/MetaMask-QA-info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 15.1; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - LIBRARY_SEARCH_PATHS = ( - "$(SDKROOT)/usr/lib/swift", - "$(inherited)", - "\"$(SRCROOT)/MetaMask/System/Library/Frameworks\"", - ); - LLVM_LTO = YES; - MARKETING_VERSION = 7.78.0; - ONLY_ACTIVE_ARCH = YES; - OTHER_CFLAGS = ( - "$(inherited)", - "-DFB_SONARKIT_ENABLED=1", - ); - OTHER_LDFLAGS = ( - "$(inherited)", - "-ObjC", - "-lc++", - ); - OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; - PRODUCT_BUNDLE_IDENTIFIER = "io.metamask.MetaMask-QA"; - PRODUCT_NAME = "$(TARGET_NAME)"; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "development-metamask-qa"; - SWIFT_OBJC_BRIDGING_HEADER = "MetaMask-Bridging-Header.h"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Debug; - }; - B339FF38289ABD70001B89FB /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = CF014205BB8964CFE74D4D8E /* Pods-MetaMask-QA.release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-QA"; - ASSETCATALOG_COMPILER_OPTIMIZATION = time; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; - CODE_SIGN_IDENTITY = "iPhone Distribution"; - CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 4823; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = 48XVW22RCG; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)", - ); - GCC_PRECOMPILE_PREFIX_HEADER = YES; - GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)"; - GCC_UNROLL_LOOPS = YES; - HEADER_SEARCH_PATHS = ( - "$(inherited)", - "$(SRCROOT)/../node_modules/react-native-wkwebview-reborn/ios/RCTWKWebView", - "$(SRCROOT)/../node_modules/react-native-keychain/RNKeychainManager", - "$(SRCROOT)/../node_modules/react-native-share/ios", - "$(SRCROOT)/../node_modules/react-native-branch/ios/**", - "$(SRCROOT)/../node_modules/@metamask/react-native-search-api/ios/RCTSearchApi", - "$(SRCROOT)/../node_modules/lottie-ios/lottie-ios/Classes/**", - "$(SRCROOT)/../node_modules/react-native-view-shot/ios", - "$(SRCROOT)/../node_modules/react-native-tcp/ios/**", - ); - INFOPLIST_FILE = "MetaMask/MetaMask-QA-info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 15.1; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - LIBRARY_SEARCH_PATHS = ( - "$(SDKROOT)/usr/lib/swift", - "$(inherited)", - "\"$(SRCROOT)/MetaMask/System/Library/Frameworks\"", - ); - LLVM_LTO = YES; - MARKETING_VERSION = 7.78.0; - ONLY_ACTIVE_ARCH = NO; - OTHER_CFLAGS = ( - "$(inherited)", - "-DFB_SONARKIT_ENABLED=1", - ); - OTHER_LDFLAGS = ( - "$(inherited)", - "-ObjC", - "-lc++", - ); - OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; - PRODUCT_BUNDLE_IDENTIFIER = "io.metamask.MetaMask-QA"; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = "Bitrise Internal Release - MetaMask-QA"; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Bitrise Internal Release - MetaMask-QA"; - SWIFT_OBJC_BRIDGING_HEADER = "MetaMask-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Release; - }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -1803,15 +1374,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Debug; }; - B339FF36289ABD70001B89FB /* Build configuration list for PBXNativeTarget "MetaMask-QA" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - B339FF37289ABD70001B89FB /* Debug */, - B339FF38289ABD70001B89FB /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Debug; - }; /* End XCConfigurationList section */ }; rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; diff --git a/ios/MetaMask.xcodeproj/xcshareddata/xcschemes/MetaMask-QA.xcscheme b/ios/MetaMask.xcodeproj/xcshareddata/xcschemes/MetaMask-QA.xcscheme deleted file mode 100644 index 85b71282b08..00000000000 --- a/ios/MetaMask.xcodeproj/xcshareddata/xcschemes/MetaMask-QA.xcscheme +++ /dev/null @@ -1,96 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/100.png b/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/100.png deleted file mode 100644 index 9b18b56b626..00000000000 Binary files a/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/100.png and /dev/null differ diff --git a/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/1024.png b/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/1024.png deleted file mode 100644 index a44844b9799..00000000000 Binary files a/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/1024.png and /dev/null differ diff --git a/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/114.png b/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/114.png deleted file mode 100644 index f2badb6f5c6..00000000000 Binary files a/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/114.png and /dev/null differ diff --git a/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/120.png b/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/120.png deleted file mode 100644 index ffc4050b445..00000000000 Binary files a/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/120.png and /dev/null differ diff --git a/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/144.png b/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/144.png deleted file mode 100644 index e47592f50fd..00000000000 Binary files a/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/144.png and /dev/null differ diff --git a/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/152.png b/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/152.png deleted file mode 100644 index 039c66b6d2e..00000000000 Binary files a/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/152.png and /dev/null differ diff --git a/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/167.png b/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/167.png deleted file mode 100644 index a3be48e551a..00000000000 Binary files a/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/167.png and /dev/null differ diff --git a/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/180.png b/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/180.png deleted file mode 100644 index 729e8f9b480..00000000000 Binary files a/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/180.png and /dev/null differ diff --git a/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/20.png b/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/20.png deleted file mode 100644 index ed45b56ce53..00000000000 Binary files a/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/20.png and /dev/null differ diff --git a/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/29.png b/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/29.png deleted file mode 100644 index 7512c1673ec..00000000000 Binary files a/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/29.png and /dev/null differ diff --git a/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/40.png b/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/40.png deleted file mode 100644 index 1ad5b4b70f4..00000000000 Binary files a/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/40.png and /dev/null differ diff --git a/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/50.png b/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/50.png deleted file mode 100644 index 7ea9d7ebafc..00000000000 Binary files a/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/50.png and /dev/null differ diff --git a/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/57.png b/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/57.png deleted file mode 100644 index 05a48bd90b1..00000000000 Binary files a/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/57.png and /dev/null differ diff --git a/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/58.png b/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/58.png deleted file mode 100644 index 838899d0e63..00000000000 Binary files a/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/58.png and /dev/null differ diff --git a/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/60.png b/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/60.png deleted file mode 100644 index 49a4fb0a046..00000000000 Binary files a/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/60.png and /dev/null differ diff --git a/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/72.png b/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/72.png deleted file mode 100644 index 3816538802d..00000000000 Binary files a/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/72.png and /dev/null differ diff --git a/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/76.png b/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/76.png deleted file mode 100644 index 49be9559e7f..00000000000 Binary files a/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/76.png and /dev/null differ diff --git a/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/80.png b/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/80.png deleted file mode 100644 index 9b15ae76684..00000000000 Binary files a/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/80.png and /dev/null differ diff --git a/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/87.png b/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/87.png deleted file mode 100644 index 73e4a1fb93e..00000000000 Binary files a/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/87.png and /dev/null differ diff --git a/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/Contents.json b/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/Contents.json deleted file mode 100644 index 65b74d7ef11..00000000000 --- a/ios/MetaMask/Images.xcassets/AppIcon-QA.appiconset/Contents.json +++ /dev/null @@ -1 +0,0 @@ -{"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"72x72","expected-size":"72","filename":"72.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"76x76","expected-size":"152","filename":"152.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"50x50","expected-size":"100","filename":"100.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"76x76","expected-size":"76","filename":"76.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"50x50","expected-size":"50","filename":"50.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"72x72","expected-size":"144","filename":"144.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"40x40","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"83.5x83.5","expected-size":"167","filename":"167.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"20x20","expected-size":"20","filename":"20.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"}]} \ No newline at end of file diff --git a/ios/MetaMask/IosExportOptionsMetaMaskQADevelopment.plist b/ios/MetaMask/IosExportOptionsMetaMaskQADevelopment.plist deleted file mode 100644 index c23bf5e9d5a..00000000000 --- a/ios/MetaMask/IosExportOptionsMetaMaskQADevelopment.plist +++ /dev/null @@ -1,17 +0,0 @@ - - - - - provisioningProfiles - - io.metamask.MetaMask-QA - development-metamask-qa - - uploadSymbols - - teamID - 48XVW22RCG - method - development - - diff --git a/ios/MetaMask/IosExportOptionsMetaMaskQARelease.plist b/ios/MetaMask/IosExportOptionsMetaMaskQARelease.plist deleted file mode 100644 index e9a6e25cefc..00000000000 --- a/ios/MetaMask/IosExportOptionsMetaMaskQARelease.plist +++ /dev/null @@ -1,17 +0,0 @@ - - - - - provisioningProfiles - - io.metamask.MetaMask-QA - Bitrise Internal Release - MetaMask-QA - - uploadSymbols - - teamID - 48XVW22RCG - method - ad-hoc - - diff --git a/ios/MetaMask/MetaMask-QA-Info.plist b/ios/MetaMask/MetaMask-QA-Info.plist deleted file mode 100644 index d903698036a..00000000000 --- a/ios/MetaMask/MetaMask-QA-Info.plist +++ /dev/null @@ -1,128 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleDisplayName - MetaMask QA - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - $(MARKETING_VERSION) - CFBundleSignature - ???? - CFBundleURLTypes - - - CFBundleURLSchemes - - ethereum - metamask - dapp - wc - expo-metamask - - - - CFBundleVersion - $(CURRENT_PROJECT_VERSION) - ITSAppUsesNonExemptEncryption - - LSApplicationQueriesSchemes - - twitter - itms-apps - - LSRequiresIPhoneOS - - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - NSExceptionDomains - - localhost - - NSExceptionAllowsInsecureHTTPLoads - - - - - NSBluetoothAlwaysUsageDescription - MetaMask needs Bluetooth access to connect to external devices. - NSBluetoothPeripheralUsageDescription - MetaMask needs Bluetooth access to connect to external devices. - NSCameraUsageDescription - MetaMask needs camera access to scan QR codes - NSFaceIDUsageDescription - $(PRODUCT_NAME) needs to authenticate - NSLocationWhenInUseUsageDescription - MetaMask needs location access to enable Bluetooth connectivity with hardware wallets. - NSMicrophoneUsageDescription - MetaMask needs microphone access to record audio - NSPhotoLibraryAddUsageDescription - Allow MetaMask to save an image to your Photo Library - NSPhotoLibraryUsageDescription - Allow MetaMask to access a images from your Photo Library - UIAppFonts - - Entypo.ttf - AntDesign.ttf - EvilIcons.ttf - Feather.ttf - FontAwesome.ttf - Foundation.ttf - Ionicons.ttf - MaterialCommunityIcons.ttf - MaterialIcons.ttf - Octicons.ttf - SimpleLineIcons.ttf - Zocial.ttf - Metamask.ttf - FontAwesome5_Brands.ttf - FontAwesome5_Regular.ttf - FontAwesome5_Solid.ttf - MMPoly-Regular.otf - MMSans-Bold.otf - MMSans-Medium.otf - MMSans-Regular.otf - Geist-SemiBoldItalic.otf - Geist-SemiBold.otf - Geist-MediumItalic.otf - Geist-Medium.otf - Geist-RegularItalic.otf - Geist-Regular.otf - - UILaunchStoryboardName - SplashScreen - UIRequiredDeviceCapabilities - - arm64 - - UIStatusBarStyle - UIStatusBarStyleDefault - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - - UIViewControllerBasedStatusBarAppearance - - branch_key - $(MM_BRANCH_KEY_LIVE) - fox_code - $(MM_FOX_CODE) - braze_api_key - $(MM_BRAZE_API_KEY_IOS) - braze_sdk_endpoint - $(MM_BRAZE_SDK_ENDPOINT) - - diff --git a/ios/Podfile b/ios/Podfile index 8d8f17f486f..5c9f5218f5a 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -111,10 +111,6 @@ target 'MetaMask' do common_target_logic end -target 'MetaMask-QA' do - common_target_logic -end - target 'MetaMask-Flask' do common_target_logic end diff --git a/ios/Podfile.lock b/ios/Podfile.lock index ac40f80e0cd..fd4c3f3c68f 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -4875,6 +4875,6 @@ SPEC CHECKSUMS: Yoga: 728df40394d49f3f471688747cf558158b3a3bd1 YttriumWrapper: cbddb60c835ebc4232d9f57064084ab30686a18e -PODFILE CHECKSUM: 7f9ed5da6254c5681e21cb20a82f612d69b412fe +PODFILE CHECKSUM: 03e4337c8a46ec38235554e231071c616488b29a COCOAPODS: 1.16.2 diff --git a/metro.transform.js b/metro.transform.js index 8c61e2a7407..49202c3d150 100644 --- a/metro.transform.js +++ b/metro.transform.js @@ -68,8 +68,6 @@ function getBuildTypeFeaturesFromEnv() { let features; switch (buildType) { - case 'qa': - case 'QA': case 'main': if (envType === 'exp') { features = new Set(experimentalFeatureSet); diff --git a/package.json b/package.json index 443acdbcca4..4c89af5d409 100644 --- a/package.json +++ b/package.json @@ -31,10 +31,8 @@ "setup:flask": "export METAMASK_BUILD_TYPE='flask' && yarn setup", "setup:expo": "yarn clean && node scripts/setup.mjs --no-build-ios --no-build-android", "start:ios": "./scripts/build.sh ios main dev --local", - "start:ios:qa": "./scripts/build.sh ios qa dev --local", "start:ios:flask": "./scripts/build.sh ios flask dev --local", "start:android": "./scripts/build.sh android main dev --local", - "start:android:qa": "./scripts/build.sh android qa dev --local", "start:android:flask": "./scripts/build.sh android flask dev --local", "start:api-logging-server": "node -r @babel/register scripts/start-api-logging-server.js", "ramps:debug-dashboard": "bash scripts/money-movement/debug-dashboard/run.sh", @@ -52,10 +50,7 @@ "build:android:flask:dev": "./scripts/build.sh android flask dev", "build:android:flask:test": "./scripts/build.sh android flask test", "build:android:flask:e2e": "BRIDGE_USE_DEV_APIS=true ./scripts/build.sh android flask e2e", - "build:android:qa:prod": "./scripts/build.sh android qa production", - "build:android:qa:dev": "./scripts/build.sh android qa dev", "build:android:checksum:prod": "./scripts/checksum.sh", - "build:android:checksum:qa": "./scripts/checksum.sh QA", "build:android:checksum:flask": "export METAMASK_BUILD_TYPE='flask' && ./scripts/checksum.sh flask", "build:android:checksum:verify": "shasum -a 512 -c sha512sums.txt", "fingerprint:generate": "node scripts/generate-fingerprint.js", @@ -80,12 +75,7 @@ "build:ios:flask:dev": "./scripts/build.sh ios flask dev", "build:ios:flask:test": "./scripts/build.sh ios flask test", "build:ios:flask:e2e": "BRIDGE_USE_DEV_APIS=true ./scripts/build.sh ios flask e2e", - "build:ios:qa:prod": "./scripts/build.sh ios qa production", - "build:ios:qa:dev": "./scripts/build.sh ios qa dev", - "build:ios:pre-qa": "./scripts/build.sh ios QA --pre", "build:ios:pre-flask": "export METAMASK_BUILD_TYPE='flask' && ./scripts/build.sh ios flask --pre", - "build:android:qa": "./scripts/build.sh android QA", - "build:ios:qa": "./scripts/build.sh ios QA", "build:attribution": "./scripts/generate-attributions.sh", "test": "yarn test:unit", "test:unit": "jest ./app/ ./locales/ ./tests/**/*.test.ts .github/**/*.test.ts ./scripts/**/*.test.ts --testPathIgnorePatterns='.*/tests/(smoke|regression)/.*\\.spec\\.(ts|tsx|js)$|.*/e2e/.*\\.spec\\.(ts|tsx|js)$|.*\\.view(\\..*)?\\.test\\.(ts|tsx|js|jsx)$'", diff --git a/scripts/build.sh b/scripts/build.sh index 62ead67479b..7ba6c6ad2c4 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -144,10 +144,10 @@ checkParameters(){ exit 1 esac - VALID_METAMASK_BUILD_TYPES="main|flask|qa" + VALID_METAMASK_BUILD_TYPES="main|flask" # Check if the METAMASK_BUILD_TYPE is valid case "${METAMASK_BUILD_TYPE}" in - main|flask|qa) + main|flask) # Valid build type - continue ;; *) @@ -209,8 +209,8 @@ loadBuildConfig() { # Legacy env remapping (Bitrise). Used only when GITHUB_ACTIONS is not set. # GitHub Actions uses loadBuildConfig + builds.yml; secrets are set with canonical names. # ───────────────────────────────────────────────────────────────────────────── -# Remap Bitrise-style vars (*_DEV, *_QA, *_PROD) to canonical names. Skip when source is unset -# (local / builds.yml use canonical names in .js.env; no _DEV/_QA needed). +# Remap Bitrise-style vars (*_DEV, *_PROD) to canonical names. Skip when source is unset +# (local / builds.yml use canonical names in .js.env; no _DEV/_PROD needed). # Legacy path (not GHA, not builds.yml): missing source var fails fast. Local: set BUILDS_ENABLED_WITH_GH_ACTIONS_TEMPORARY in .js.env to use builds.yml and skip. remapEnvVariable() { local old_var_name="$1" @@ -240,15 +240,6 @@ remapMainDevEnvVariables() { remapEnvVariable "MM_CARD_BAANX_API_CLIENT_KEY_DEV" "MM_CARD_BAANX_API_CLIENT_KEY" } -remapEnvVariableQA() { - echo "Remapping QA env variable names to match QA values" - remapEnvVariable "SEGMENT_WRITE_KEY_QA" "SEGMENT_WRITE_KEY" - remapEnvVariable "SEGMENT_PROXY_URL_QA" "SEGMENT_PROXY_URL" - remapEnvVariable "SEGMENT_DELETE_API_SOURCE_ID_QA" "SEGMENT_DELETE_API_SOURCE_ID" - remapEnvVariable "SEGMENT_REGULATIONS_ENDPOINT_QA" "SEGMENT_REGULATIONS_ENDPOINT" - remapEnvVariable "MM_CARD_BAANX_API_CLIENT_KEY_UAT" "MM_CARD_BAANX_API_CLIENT_KEY" -} - # Mapping for Main env variables in the e2e environment remapMainE2EEnvVariables() { echo "Remapping Main target environment variables for the e2e environment" @@ -395,13 +386,6 @@ buildAndroidMainLocal(){ yarn expo run:android --no-install --port $WATCHER_PORT --variant 'prodDebug' --device } -# Builds and installs the QA APK for local development -buildAndroidQALocal(){ - prebuild_android - #react-native run-android --port=$WATCHER_PORT --variant=qaDebug --active-arch-only - yarn expo run:android --no-install --port $WATCHER_PORT --variant 'qaDebug' --device -} - # Builds and installs the Flask APK for local development buildAndroidFlaskLocal(){ prebuild_android @@ -421,12 +405,6 @@ buildIosFlaskLocal(){ yarn expo run:ios --no-install --configuration Debug --port $WATCHER_PORT --scheme "MetaMask-Flask" --device "$IOS_SIMULATOR" } -# Builds and installs the QA iOS app for local development -buildIosQALocal(){ - prebuild_ios - yarn expo run:ios --no-install --configuration Debug --port $WATCHER_PORT --scheme "MetaMask-QA" --device "$IOS_SIMULATOR" -} - # Generates the iOS binary for the given scheme and configuration generateIosBinary() { scheme="$1" @@ -440,9 +418,9 @@ generateIosBinary() { fi # Check if scheme is valid - if [ "$scheme" != "MetaMask" ] && [ "$scheme" != "MetaMask-QA" ] && [ "$scheme" != "MetaMask-Flask" ] ; then + if [ "$scheme" != "MetaMask" ] && [ "$scheme" != "MetaMask-Flask" ] ; then # Scheme is not recognized - echo "Scheme $scheme is not recognized! Only MetaMask, MetaMask-QA, and MetaMask-Flask are supported" + echo "Scheme $scheme is not recognized! Only MetaMask, and MetaMask-Flask are supported" exit 1 fi @@ -455,12 +433,6 @@ generateIosBinary() { else exportOptionsPlist="MetaMask/IosExportOptionsMetaMaskRelease.plist" fi - elif [ "$scheme" = "MetaMask-QA" ] ; then - if [ "$profile" = "development" ] ; then - exportOptionsPlist="MetaMask/IosExportOptionsMetaMaskQADevelopment.plist" - else - exportOptionsPlist="MetaMask/IosExportOptionsMetaMaskQARelease.plist" - fi elif [ "$scheme" = "MetaMask-Flask" ] ; then if [ "$profile" = "development" ] ; then exportOptionsPlist="MetaMask/IosExportOptionsMetaMaskFlaskDevelopment.plist" @@ -501,7 +473,7 @@ generateIosBinary() { # Generates the Android binary for the given scheme and configuration generateAndroidBinary() { - # Prod, Flask, or QA (Deprecated - Do not use) + # Prod, Flask local flavor="$1" # Lowercase flavor string local lowercaseFlavor=$(echo "$flavor" | tr '[:upper:]' '[:lower:]') @@ -530,9 +502,9 @@ generateAndroidBinary() { fi # Check if flavor is valid - if [ "$flavor" != "Prod" ] && [ "$flavor" != "Flask" ] && [ "$flavor" != "Qa" ] ; then + if [ "$flavor" != "Prod" ] && [ "$flavor" != "Flask" ] ; then # Flavor is not recognized - echo "Flavor $flavor is not recognized! Only Prod, Flask, and Qa (Deprecated - Do not use) are supported" + echo "Flavor $flavor is not recognized! Only Prod, Flask are supported" exit 1 fi @@ -799,17 +771,6 @@ buildAndroid() { # Generate Android binary generateAndroidBinary "Flask" fi - elif [ "$METAMASK_BUILD_TYPE" == "QA" ] || [ "$METAMASK_BUILD_TYPE" == "qa" ] ; then - if [ "$IS_LOCAL" = true ] ; then - buildAndroidQALocal - else - # Prepare Android dependencies - prebuild_android - # Go to android directory - cd android - # Generate Android binary - generateAndroidBinary "Qa" - fi else printError "METAMASK_BUILD_TYPE '${METAMASK_BUILD_TYPE}' is not recognized." exit 1 @@ -840,17 +801,6 @@ buildIos() { # Generate iOS binary generateIosBinary "MetaMask-Flask" fi - elif [ "$METAMASK_BUILD_TYPE" == "QA" ] || [ "$METAMASK_BUILD_TYPE" == "qa" ] ; then - if [ "$IS_LOCAL" = true ] ; then - buildIosQALocal - else - # Prepare iOS dependencies - prebuild_ios - # Go to ios directory - cd ios - # Generate iOS binary - generateIosBinary "MetaMask-QA" - fi else printError "METAMASK_BUILD_TYPE '${METAMASK_BUILD_TYPE}' is not recognized" exit 1 @@ -972,8 +922,6 @@ if [ "$PLATFORM" != "expo-update" ]; then elif [ "$METAMASK_ENVIRONMENT" == "e2e" ]; then remapFlaskE2EEnvVariables fi - elif [ "$METAMASK_BUILD_TYPE" == "qa" ] || [ "$METAMASK_BUILD_TYPE" == "QA" ]; then - remapEnvVariableQA fi fi fi @@ -985,14 +933,14 @@ if [ "$METAMASK_ENVIRONMENT" == "e2e" ]; then export IGNORE_BOXLOGS_DEVELOPMENT="true" fi -if [ "$METAMASK_BUILD_TYPE" == "QA" ]; then - echo "DEBUG SENTRY PROPS" - checkAuthToken 'sentry.debug.properties' - export SENTRY_PROPERTIES="${REPO_ROOT_DIR}/sentry.debug.properties" -elif [ "$METAMASK_BUILD_TYPE" == "flask" ] || [ "$METAMASK_BUILD_TYPE" == "main" ]; then +if [ "$METAMASK_ENVIRONMENT" == "production" ]; then echo "RELEASE SENTRY PROPS" checkAuthToken 'sentry.release.properties' export SENTRY_PROPERTIES="${REPO_ROOT_DIR}/sentry.release.properties" +else + echo "DEBUG SENTRY PROPS" + checkAuthToken 'sentry.debug.properties' + export SENTRY_PROPERTIES="${REPO_ROOT_DIR}/sentry.debug.properties" fi # Update Expo channel configuration based on environment diff --git a/scripts/rename-artifacts.js b/scripts/rename-artifacts.js index 14f60d3ca27..92063e6f845 100644 --- a/scripts/rename-artifacts.js +++ b/scripts/rename-artifacts.js @@ -110,9 +110,6 @@ function renameAndroid() { case 'flask': appFlavor = 'flask'; break; - case 'qa': - appFlavor = 'qa'; - break; default: console.error(`❌ Unknown build type: ${buildType}`); process.exit(1); @@ -237,9 +234,6 @@ function renameIos() { case 'flask': appName = 'MetaMask-Flask'; break; - case 'qa': - appName = 'MetaMask-QA'; - break; default: console.error(`❌ Unknown build type: ${buildType}`); process.exit(1); diff --git a/scripts/repack.js b/scripts/repack.js index 43f139e4a95..49908a6ece3 100644 --- a/scripts/repack.js +++ b/scripts/repack.js @@ -218,8 +218,9 @@ async function repackIos() { throw new Error( `Repacked app is missing its bundle executable at "${executablePath}". ` + `@expo/repack-app may have dropped the binary (possible symlink handling issue). ` + - `Aborting to prevent uploading a broken artifact — bust IOS_APP_CACHE_VERSION ` + - `in build-ios-e2e.yml to force a full rebuild.` + `Aborting to prevent uploading a broken artifact — add the \`force-builds\` ` + + `label (or a \`[force-builds]\` token in the commit message) to the PR to ` + + `bypass cross-run artifact reuse and force a full native rebuild.` ); } logger.success(`Bundle executable verified: ${sourceAppName}`);