From c2e03b737fb610a21c7d1febf3a2dcccfaffa52a Mon Sep 17 00:00:00 2001 From: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> Date: Tue, 23 Sep 2025 15:48:39 +0700 Subject: [PATCH 01/50] Create config.yml (#56) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> --- .circleci/config.yml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .circleci/config.yml diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000000..d5d401c5189 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,31 @@ +# Use the latest 2.1 version of CircleCI pipeline process engine. +# See: https://circleci.com/docs/configuration-reference +version: 2.1 + +# Define a job to be invoked later in a workflow. +# See: https://circleci.com/docs/jobs-steps/#jobs-overview & https://circleci.com/docs/configuration-reference/#jobs +jobs: + say-hello: + # Specify the execution environment. You can specify an image from Docker Hub or use one of our convenience images from CircleCI's Developer Hub. + # See: https://circleci.com/docs/executor-intro/ & https://circleci.com/docs/configuration-reference/#executor-job + docker: + # Specify the version you desire here + # See: https://circleci.com/developer/images/image/cimg/base + - image: cimg/base:current + + # Add steps to the job + # See: https://circleci.com/docs/jobs-steps/#steps-overview & https://circleci.com/docs/configuration-reference/#steps + steps: + # Checkout the code as the first step. + - checkout + - run: + name: "Say hello" + command: "echo Hello, World!" + +# Orchestrate jobs using workflows +# See: https://circleci.com/docs/workflows/ & https://circleci.com/docs/configuration-reference/#workflows +workflows: + say-hello-workflow: # This is the name of the workflow, feel free to change it to better match your workflow. + # Inside the workflow, you define the jobs you want to run. + jobs: + - say-hello From b7ece147ee4072d81d137dcb182f5b21268a7f46 Mon Sep 17 00:00:00 2001 From: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> Date: Sun, 28 Sep 2025 14:32:25 +0700 Subject: [PATCH 02/50] fix: apps/mobile/Gemfile & apps/mobile/Gemfile.lock to reduce vulnerabilities (#60) The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-RUBY-REXML-12878608 - https://snyk.io/vuln/SNYK-RUBY-WEBRICK-10500756 - https://snyk.io/vuln/SNYK-RUBY-WEBRICK-8068535 Co-authored-by: snyk-bot --- apps/mobile/Gemfile | 4 +- apps/mobile/Gemfile.lock | 184 ++++++++++++++++++++------------------- 2 files changed, 98 insertions(+), 90 deletions(-) diff --git a/apps/mobile/Gemfile b/apps/mobile/Gemfile index a07e6ca69f3..a9e494ea9a1 100644 --- a/apps/mobile/Gemfile +++ b/apps/mobile/Gemfile @@ -1,8 +1,8 @@ source "https://rubygems.org" -gem 'fastlane', '2.214.0' +gem 'fastlane', '2.215.0' # Exclude problematic versions of cocoapods and activesupport that causes build failures. -gem 'cocoapods', '1.14.3' +gem 'cocoapods', '1.15.0' gem 'activesupport', '7.1.2' gem 'xcodeproj', '1.27.0' gem 'concurrent-ruby', '1.3.4' diff --git a/apps/mobile/Gemfile.lock b/apps/mobile/Gemfile.lock index b5891798a1a..c99f3366066 100644 --- a/apps/mobile/Gemfile.lock +++ b/apps/mobile/Gemfile.lock @@ -1,7 +1,9 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.6) + CFPropertyList (3.0.7) + base64 + nkf rexml activesupport (7.1.2) base64 @@ -13,37 +15,40 @@ GEM minitest (>= 5.1) mutex_m tzinfo (~> 2.0) - addressable (2.8.6) - public_suffix (>= 2.0.2, < 6.0) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) algoliasearch (1.27.5) httpclient (~> 2.8, >= 2.8.3) json (>= 1.5.1) - artifactory (3.0.15) + artifactory (3.0.17) atomos (0.1.3) - aws-eventstream (1.3.0) - aws-partitions (1.877.0) - aws-sdk-core (3.190.1) + aws-eventstream (1.4.0) + aws-partitions (1.1166.0) + aws-sdk-core (3.233.0) aws-eventstream (~> 1, >= 1.3.0) - aws-partitions (~> 1, >= 1.651.0) - aws-sigv4 (~> 1.8) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) + base64 + bigdecimal jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.75.0) - aws-sdk-core (~> 3, >= 3.188.0) - aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.142.0) - aws-sdk-core (~> 3, >= 3.189.0) + logger + aws-sdk-kms (1.113.0) + aws-sdk-core (~> 3, >= 3.231.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.199.1) + aws-sdk-core (~> 3, >= 3.231.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.8) - aws-sigv4 (1.8.0) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.12.1) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) - base64 (0.2.0) - bigdecimal (3.1.9) + base64 (0.3.0) + bigdecimal (3.2.3) claide (1.1.0) - cocoapods (1.14.3) + cocoapods (1.15.0) addressable (~> 2.8) claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.14.3) + cocoapods-core (= 1.15.0) cocoapods-deintegrate (>= 1.0.3, < 2.0) cocoapods-downloader (>= 2.1, < 3.0) cocoapods-plugins (>= 1.0.0, < 2.0) @@ -58,7 +63,7 @@ GEM nap (~> 1.0) ruby-macho (>= 2.3.0, < 3.0) xcodeproj (>= 1.23.0, < 2.0) - cocoapods-core (1.14.3) + cocoapods-core (1.15.0) activesupport (>= 5.0, < 8) addressable (~> 2.8) algoliasearch (~> 1.0) @@ -82,19 +87,19 @@ GEM commander (4.6.0) highline (~> 2.0.0) concurrent-ruby (1.3.4) - connection_pool (2.5.0) + connection_pool (2.5.4) declarative (0.0.20) - digest-crc (0.6.5) + digest-crc (0.7.0) rake (>= 12.0.0, < 14.0.0) - domain_name (0.6.20231109) + domain_name (0.6.20240107) dotenv (2.8.1) - drb (2.2.1) + drb (2.2.3) emoji_regex (3.2.3) escape (0.0.4) - ethon (0.16.0) + ethon (0.15.0) ffi (>= 1.15.0) - excon (0.109.0) - faraday (1.10.3) + excon (0.112.0) + faraday (1.10.4) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) @@ -110,20 +115,20 @@ GEM faraday (>= 0.8.0) http-cookie (~> 1.0.0) faraday-em_http (1.0.0) - faraday-em_synchrony (1.0.0) + faraday-em_synchrony (1.0.1) faraday-excon (1.1.0) faraday-httpclient (1.0.1) - faraday-multipart (1.0.4) - multipart-post (~> 2) - faraday-net_http (1.0.1) + faraday-multipart (1.1.1) + multipart-post (~> 2.0) + faraday-net_http (1.0.2) faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) faraday-rack (1.0.0) faraday-retry (1.0.3) - faraday_middleware (1.2.0) + faraday_middleware (1.2.1) faraday (~> 1.0) - fastimage (2.3.0) - fastlane (2.214.0) + fastimage (2.4.0) + fastlane (2.215.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -144,6 +149,7 @@ GEM google-apis-playcustomapp_v1 (~> 0.1) google-cloud-storage (~> 1.31) highline (~> 2.0) + http-cookie (~> 1.0.5) json (< 3.0.0) jwt (>= 2.1.0, < 3) mini_magick (>= 4.9.4, < 5.0.0) @@ -155,116 +161,120 @@ GEM security (= 0.1.3) simctl (~> 1.6.3) terminal-notifier (>= 2.0.0, < 3.0.0) - terminal-table (>= 1.4.5, < 2.0.0) + terminal-table (~> 3) tty-screen (>= 0.6.3, < 1.0.0) tty-spinner (>= 0.8.0, < 1.0.0) word_wrap (~> 1.0.0) xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.3.0) xcpretty-travis-formatter (>= 0.0.3) - fastlane-plugin-get_version_name (0.2.2) - fastlane-plugin-versioning_android (0.1.1) - ffi (1.17.1) + ffi (1.17.2) fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.54.0) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-core (0.11.2) + google-apis-androidpublisher_v3 (0.87.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-core (0.18.0) addressable (~> 2.5, >= 2.5.1) - googleauth (>= 0.16.2, < 2.a) - httpclient (>= 2.8.1, < 3.a) + googleauth (~> 1.9) + httpclient (>= 2.8.3, < 3.a) mini_mime (~> 1.0) + mutex_m representable (~> 3.0) retriable (>= 2.0, < 4.a) - rexml - webrick - google-apis-iamcredentials_v1 (0.17.0) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-playcustomapp_v1 (0.13.0) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-storage_v1 (0.29.0) - google-apis-core (>= 0.11.0, < 2.a) - google-cloud-core (1.6.1) + google-apis-iamcredentials_v1 (0.24.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-playcustomapp_v1 (0.17.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-storage_v1 (0.56.0) + google-apis-core (>= 0.15.0, < 2.a) + google-cloud-core (1.8.0) google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) - google-cloud-env (2.1.0) + google-cloud-env (2.3.1) + base64 (~> 0.2) faraday (>= 1.0, < 3.a) - google-cloud-errors (1.3.1) - google-cloud-storage (1.45.0) + google-cloud-errors (1.5.0) + google-cloud-storage (1.57.0) addressable (~> 2.8) digest-crc (~> 0.4) - google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.29.0) + google-apis-core (>= 0.18, < 2) + google-apis-iamcredentials_v1 (~> 0.18) + google-apis-storage_v1 (>= 0.42) google-cloud-core (~> 1.6) - googleauth (>= 0.16.2, < 2.a) + googleauth (~> 1.9) mini_mime (~> 1.0) - googleauth (1.9.1) + google-logging-utils (0.2.0) + googleauth (1.15.0) faraday (>= 1.0, < 3.a) - google-cloud-env (~> 2.1) - jwt (>= 1.4, < 3.0) + google-cloud-env (~> 2.2) + google-logging-utils (~> 0.1) + jwt (>= 1.4, < 4.0) multi_json (~> 1.11) os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) highline (2.0.3) - http-cookie (1.0.5) + http-cookie (1.0.8) domain_name (~> 0.5) - httpclient (2.8.3) + httpclient (2.9.0) + mutex_m i18n (1.14.7) concurrent-ruby (~> 1.0) jmespath (1.6.2) - json (2.7.1) - jwt (2.7.1) - mini_magick (4.12.0) + json (2.15.0) + jwt (2.10.2) + base64 + logger (1.7.0) + mini_magick (4.13.2) mini_mime (1.1.5) - minitest (5.25.4) + minitest (5.25.5) molinillo (0.8.0) - multi_json (1.15.0) - multipart-post (2.3.0) + multi_json (1.17.0) + multipart-post (2.4.1) mutex_m (0.3.0) nanaimo (0.4.0) nap (1.1.0) - naturally (2.2.1) + naturally (2.3.0) netrc (0.11.0) + nkf (0.2.0) optparse (0.1.1) os (1.1.4) - plist (3.7.1) + plist (3.7.2) public_suffix (4.0.7) - rake (13.1.0) + rake (13.3.0) representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.4.1) + rexml (3.4.4) rouge (2.0.7) ruby-macho (2.5.1) ruby2_keywords (0.0.5) - rubyzip (2.3.2) + rubyzip (2.4.1) security (0.1.3) - signet (0.18.0) + signet (0.21.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) - jwt (>= 1.5, < 3.0) + jwt (>= 1.5, < 4.0) multi_json (~> 1.10) simctl (1.6.10) CFPropertyList naturally terminal-notifier (2.0.0) - terminal-table (1.8.0) - unicode-display_width (~> 1.1, >= 1.1.1) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) trailblazer-option (0.1.2) tty-cursor (0.7.1) tty-screen (0.8.2) tty-spinner (0.9.3) tty-cursor (~> 0.7) - typhoeus (1.4.1) - ethon (>= 0.9.0) + typhoeus (1.5.0) + ethon (>= 0.9.0, < 0.16.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) uber (0.1.0) - unicode-display_width (1.8.0) - webrick (1.8.1) + unicode-display_width (2.6.0) word_wrap (1.0.0) xcodeproj (1.27.0) CFPropertyList (>= 2.3.3, < 4.0) @@ -283,12 +293,10 @@ PLATFORMS DEPENDENCIES activesupport (= 7.1.2) - cocoapods (= 1.14.3) + cocoapods (= 1.15.0) concurrent-ruby (= 1.3.4) - fastlane (= 2.214.0) - fastlane-plugin-get_version_name - fastlane-plugin-versioning_android + fastlane (= 2.215.0) xcodeproj (= 1.27.0) BUNDLED WITH - 2.4.10 + 2.3.26 From 58d9f6c20cf4711bd922c35668c9e4d3c7f7b513 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 28 Sep 2025 19:39:15 +0700 Subject: [PATCH 03/50] build(deps): bump the npm_and_yarn group across 6 directories with 5 updates (#61) Bumps the npm_and_yarn group with 2 updates in the /apps/extension directory: [webpack](https://github.com/webpack/webpack) and [webpack-dev-server](https://github.com/webpack/webpack-dev-server). Bumps the npm_and_yarn group with 1 update in the /apps/mobile directory: [react-native-mmkv](https://github.com/mrousavy/react-native-mmkv). Bumps the npm_and_yarn group with 3 updates in the /apps/web directory: [webpack](https://github.com/webpack/webpack), [graphql](https://github.com/graphql/graphql-js) and [hono](https://github.com/honojs/hono). Bumps the npm_and_yarn group with 2 updates in the /packages/uniswap directory: [react-native-mmkv](https://github.com/mrousavy/react-native-mmkv) and [graphql](https://github.com/graphql/graphql-js). Bumps the npm_and_yarn group with 1 update in the /packages/utilities directory: [graphql](https://github.com/graphql/graphql-js). Bumps the npm_and_yarn group with 1 update in the /packages/wallet directory: [graphql](https://github.com/graphql/graphql-js). Updates `webpack` from 5.90.0 to 5.94.0 - [Release notes](https://github.com/webpack/webpack/releases) - [Commits](https://github.com/webpack/webpack/compare/v5.90.0...v5.94.0) Updates `webpack-dev-server` from 4.15.1 to 5.2.1 - [Release notes](https://github.com/webpack/webpack-dev-server/releases) - [Changelog](https://github.com/webpack/webpack-dev-server/blob/main/CHANGELOG.md) - [Commits](https://github.com/webpack/webpack-dev-server/compare/v4.15.1...v5.2.1) Updates `react-native-mmkv` from 2.10.1 to 2.11.0 - [Release notes](https://github.com/mrousavy/react-native-mmkv/releases) - [Commits](https://github.com/mrousavy/react-native-mmkv/compare/v2.10.1...v2.11.0) Updates `webpack` from 5.90.0 to 5.94.0 - [Release notes](https://github.com/webpack/webpack/releases) - [Commits](https://github.com/webpack/webpack/compare/v5.90.0...v5.94.0) Updates `graphql` from 16.6.0 to 16.8.1 - [Release notes](https://github.com/graphql/graphql-js/releases) - [Commits](https://github.com/graphql/graphql-js/compare/v16.6.0...v16.8.1) Updates `hono` from 4.8.4 to 4.9.7 - [Release notes](https://github.com/honojs/hono/releases) - [Commits](https://github.com/honojs/hono/compare/v4.8.4...v4.9.7) Updates `react-native-mmkv` from 2.10.1 to 2.11.0 - [Release notes](https://github.com/mrousavy/react-native-mmkv/releases) - [Commits](https://github.com/mrousavy/react-native-mmkv/compare/v2.10.1...v2.11.0) Updates `graphql` from 16.6.0 to 16.8.1 - [Release notes](https://github.com/graphql/graphql-js/releases) - [Commits](https://github.com/graphql/graphql-js/compare/v16.6.0...v16.8.1) Updates `graphql` from 16.6.0 to 16.8.1 - [Release notes](https://github.com/graphql/graphql-js/releases) - [Commits](https://github.com/graphql/graphql-js/compare/v16.6.0...v16.8.1) Updates `graphql` from 16.6.0 to 16.8.1 - [Release notes](https://github.com/graphql/graphql-js/releases) - [Commits](https://github.com/graphql/graphql-js/compare/v16.6.0...v16.8.1) --- updated-dependencies: - dependency-name: webpack dependency-version: 5.94.0 dependency-type: direct:development dependency-group: npm_and_yarn - dependency-name: webpack-dev-server dependency-version: 5.2.1 dependency-type: direct:development dependency-group: npm_and_yarn - dependency-name: react-native-mmkv dependency-version: 2.11.0 dependency-type: direct:production dependency-group: npm_and_yarn - dependency-name: webpack dependency-version: 5.94.0 dependency-type: direct:development dependency-group: npm_and_yarn - dependency-name: graphql dependency-version: 16.8.1 dependency-type: direct:production dependency-group: npm_and_yarn - dependency-name: hono dependency-version: 4.9.7 dependency-type: direct:production dependency-group: npm_and_yarn - dependency-name: react-native-mmkv dependency-version: 2.11.0 dependency-type: direct:production dependency-group: npm_and_yarn - dependency-name: graphql dependency-version: 16.8.1 dependency-type: direct:production dependency-group: npm_and_yarn - dependency-name: graphql dependency-version: 16.8.1 dependency-type: direct:production dependency-group: npm_and_yarn - dependency-name: graphql dependency-version: 16.8.1 dependency-type: direct:production dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/extension/package.json | 4 ++-- apps/mobile/package.json | 2 +- apps/web/package.json | 6 +++--- packages/uniswap/package.json | 4 ++-- packages/utilities/package.json | 2 +- packages/wallet/package.json | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/extension/package.json b/apps/extension/package.json index ad4bff5873a..9dcc7292896 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -93,9 +93,9 @@ "swc-loader": "0.2.6", "tamagui-loader": "1.125.17", "typescript": "5.3.3", - "webpack": "5.90.0", + "webpack": "5.94.0", "webpack-cli": "5.1.4", - "webpack-dev-server": "4.15.1" + "webpack-dev-server": "5.2.1" }, "private": true, "scripts": { diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 70aa1633b50..8d69f608761 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -152,7 +152,7 @@ "react-native-keyboard-controller": "1.17.5", "react-native-localize": "2.2.6", "react-native-markdown-display": "7.0.0-alpha.2", - "react-native-mmkv": "2.10.1", + "react-native-mmkv": "2.11.0", "react-native-onesignal": "5.2.9", "react-native-pager-view": "6.5.1", "react-native-passkey": "3.1.0", diff --git a/apps/web/package.json b/apps/web/package.json index 95063c98f64..9761dcb163d 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -150,7 +150,7 @@ "vitest": "3.2.1", "vitest-fetch-mock": "0.4.5", "wait-on": "8.0.2", - "webpack": "5.90.0", + "webpack": "5.94.0", "wrangler": "4.28.0" }, "dependencies": { @@ -222,8 +222,8 @@ "expo-crypto": "12.8.1", "fancy-canvas": "2.1.0", "framer-motion": "10.17.6", - "graphql": "16.6.0", - "hono": "4.8.4", + "graphql": "16.8.1", + "hono": "4.9.7", "html-entities": "2.6.0", "i18next": "23.10.0", "jotai": "1.3.7", diff --git a/packages/uniswap/package.json b/packages/uniswap/package.json index 134290c7a7c..b0ef401bf29 100644 --- a/packages/uniswap/package.json +++ b/packages/uniswap/package.json @@ -76,7 +76,7 @@ "expo-haptics": "14.0.1", "expo-web-browser": "14.0.2", "fuse.js": "6.5.3", - "graphql": "16.6.0", + "graphql": "16.8.1", "i18next": "23.10.0", "i18next-resources-to-backend": "1.2.1", "idb-keyval": "6.2.1", @@ -96,7 +96,7 @@ "react-native-fast-image": "8.6.3", "react-native-gesture-handler": "2.22.1", "react-native-localize": "2.2.6", - "react-native-mmkv": "2.10.1", + "react-native-mmkv": "2.11.0", "react-native-reanimated": "3.16.7", "react-native-svg": "15.11.2", "react-native-webview": "13.13.5", diff --git a/packages/utilities/package.json b/packages/utilities/package.json index 5d6ff4e591c..59fcda15023 100644 --- a/packages/utilities/package.json +++ b/packages/utilities/package.json @@ -24,7 +24,7 @@ "@uniswap/sdk-core": "7.7.2", "dayjs": "1.11.7", "expo-localization": "16.0.1", - "graphql": "16.6.0", + "graphql": "16.8.1", "jsbi": "3.2.5", "promise": "8.3.0", "react": "18.3.1", diff --git a/packages/wallet/package.json b/packages/wallet/package.json index da490de9e11..7a619b7c796 100644 --- a/packages/wallet/package.json +++ b/packages/wallet/package.json @@ -35,7 +35,7 @@ "dayjs": "1.11.7", "ethers": "5.7.2", "fuse.js": "6.5.3", - "graphql": "16.6.0", + "graphql": "16.8.1", "i18next": "23.10.0", "jsbi": "3.2.5", "lodash": "4.17.21", From 607e2db9562cc148fa60c2c5cfdd32ceb75f515f Mon Sep 17 00:00:00 2001 From: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> Date: Tue, 30 Sep 2025 02:43:18 +0700 Subject: [PATCH 04/50] Potential fix for code scanning alert no. 19: Incomplete regular expression for hostnames (#63) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- apps/web/src/pages/Landing/Landing.e2e.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/pages/Landing/Landing.e2e.test.ts b/apps/web/src/pages/Landing/Landing.e2e.test.ts index aec2b7a703b..e213ceaa6c1 100644 --- a/apps/web/src/pages/Landing/Landing.e2e.test.ts +++ b/apps/web/src/pages/Landing/Landing.e2e.test.ts @@ -53,7 +53,7 @@ test.describe('Landing Page', () => { await page.unrouteAll({ behavior: 'ignoreErrors' }) }) test('renders UK compliance banner in UK', async ({ page }) => { - await page.route(/(?:interface|beta).gateway.uniswap.org\/v1\/amplitude-proxy/, async (route) => { + await page.route(/(?:interface|beta)\.gateway\.uniswap\.org\/v1\/amplitude-proxy/, async (route) => { const requestBody = JSON.stringify(await route.request().postDataJSON()) const originalResponse = await route.fetch() const byteSize = new Blob([requestBody]).size From 036022cf0ef73728dd0d13d6855a16103ba349a6 Mon Sep 17 00:00:00 2001 From: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> Date: Tue, 30 Sep 2025 03:29:29 +0700 Subject: [PATCH 05/50] Update issue templates (#64) * Update issue templates * Update .github/ISSUE_TEMPLATE/feature_request.md Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> --------- Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/bug_report.md | 2 ++ .github/ISSUE_TEMPLATE/custom.md | 10 ++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 ++++++++++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/custom.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index b01ad10c152..feac4b614be 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -3,6 +3,8 @@ name: Bug Report about: Report a bug or unexpected behavior in the Uniswap interfaces. title: "[Bug] " labels: bug +assignees: '' + --- ## 📱 Interface Affected diff --git a/.github/ISSUE_TEMPLATE/custom.md b/.github/ISSUE_TEMPLATE/custom.md new file mode 100644 index 00000000000..48d5f81fa42 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/custom.md @@ -0,0 +1,10 @@ +--- +name: Custom issue template +about: Describe this issue template's purpose here. +title: '' +labels: '' +assignees: '' + +--- + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000000..36014cde565 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: 'enhancement' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. From 87893de63935254c9c8e4b2d6ff8d41d7bf7e4aa Mon Sep 17 00:00:00 2001 From: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> Date: Wed, 1 Oct 2025 02:22:52 +0700 Subject: [PATCH 06/50] Potential fix for code scanning alert no. 17: Incomplete regular expression for hostnames (#65) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- apps/web/src/playwright/fixtures/graphql.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/playwright/fixtures/graphql.ts b/apps/web/src/playwright/fixtures/graphql.ts index 5f2ed08e1e0..59823342aac 100644 --- a/apps/web/src/playwright/fixtures/graphql.ts +++ b/apps/web/src/playwright/fixtures/graphql.ts @@ -58,7 +58,7 @@ export const test = base.extend({ } } - await page.route(/(?:interface|beta).(gateway|api).uniswap.org\/v1\/graphql/, async (route) => { + await page.route(/(?:interface|beta)\.(gateway|api)\.uniswap\.org\/v1\/graphql/, async (route) => { const request = route.request() const postData = request.postData() if (!postData) { From b6ab792db29f627749ae76d929e914e01379c363 Mon Sep 17 00:00:00 2001 From: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> Date: Wed, 1 Oct 2025 03:17:11 +0700 Subject: [PATCH 07/50] Update tag_and_release.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> --- .github/workflows/tag_and_release.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tag_and_release.yml b/.github/workflows/tag_and_release.yml index d96e0d46fef..681f171f631 100644 --- a/.github/workflows/tag_and_release.yml +++ b/.github/workflows/tag_and_release.yml @@ -3,7 +3,10 @@ on: push: branches: - 'main' - +permissions: + contents: write + issues: write + pull-requests: write jobs: deploy-to-prod: runs-on: ubuntu-latest @@ -33,7 +36,7 @@ jobs: - name: 🪽 Release uses: actions/create-release@c9ba6969f07ed90fae07e2e66100dd03f9b1a50e env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Or use your PAT with: tag_name: ${{ steps.github-tag-action.outputs.new_tag }} release_name: Release ${{ steps.github-tag-action.outputs.new_tag }} From 8e3b4a5e184b498423d5b25ca5ceb5a9258c92c2 Mon Sep 17 00:00:00 2001 From: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> Date: Wed, 1 Oct 2025 03:30:14 +0700 Subject: [PATCH 08/50] Update tag_and_release.yml (#66) Configure tag_and_release GitHub Actions workflow with explicit write permissions and annotate the GITHUB_TOKEN usage. CI: Add explicit contents, issues, and pull-requests write permissions to the workflow Chores: Add comment noting alternative use of a personal access token for GITHUB_TOKEN Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> --- .github/workflows/tag_and_release.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tag_and_release.yml b/.github/workflows/tag_and_release.yml index d96e0d46fef..681f171f631 100644 --- a/.github/workflows/tag_and_release.yml +++ b/.github/workflows/tag_and_release.yml @@ -3,7 +3,10 @@ on: push: branches: - 'main' - +permissions: + contents: write + issues: write + pull-requests: write jobs: deploy-to-prod: runs-on: ubuntu-latest @@ -33,7 +36,7 @@ jobs: - name: 🪽 Release uses: actions/create-release@c9ba6969f07ed90fae07e2e66100dd03f9b1a50e env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Or use your PAT with: tag_name: ${{ steps.github-tag-action.outputs.new_tag }} release_name: Release ${{ steps.github-tag-action.outputs.new_tag }} From 4b5c40f67b3a55a3ddb505fe2dcc53b4dc6d9d1c Mon Sep 17 00:00:00 2001 From: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> Date: Wed, 1 Oct 2025 09:17:20 +0700 Subject: [PATCH 09/50] Update tag_and_release.yml (#67) Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> --- .github/workflows/tag_and_release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tag_and_release.yml b/.github/workflows/tag_and_release.yml index 681f171f631..ec6b8721bc6 100644 --- a/.github/workflows/tag_and_release.yml +++ b/.github/workflows/tag_and_release.yml @@ -34,7 +34,7 @@ jobs: tag_prefix: "" - name: 🪽 Release - uses: actions/create-release@c9ba6969f07ed90fae07e2e66100dd03f9b1a50e + uses: actions/create-release@8e3b4a5e184b498423d5b25ca5ceb5a9258c92c2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Or use your PAT with: From 008555b99b527a799ea1cd33771b932bd7866383 Mon Sep 17 00:00:00 2001 From: AU_019 <64915515+Dargon789@users.noreply.github.com> Date: Sun, 2 Feb 2025 01:24:29 +0700 Subject: [PATCH 10/50] Potential fix for code scanning alert no. 10: Incomplete regular expression for hostnames Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: AU_019 <64915515+Dargon789@users.noreply.github.com> --- apps/web/cypress/support/commands.ts | 168 +++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 apps/web/cypress/support/commands.ts diff --git a/apps/web/cypress/support/commands.ts b/apps/web/cypress/support/commands.ts new file mode 100644 index 00000000000..476905a0daa --- /dev/null +++ b/apps/web/cypress/support/commands.ts @@ -0,0 +1,168 @@ +import 'cypress-hardhat/lib/browser' + +import { Eip1193 } from 'cypress-hardhat/lib/browser/eip1193' +import { FeatureFlagClient, FeatureFlags, getFeatureFlagName } from 'uniswap/src/features/gating/flags' +import { ALLOW_ANALYTICS_ATOM_KEY } from 'utilities/src/telemetry/analytics/constants' +import { UserState, initialState } from '../../src/state/user/reducer' +import { setInitialUserState } from '../utils/user-state' + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Cypress { + interface ApplicationWindow { + ethereum: Eip1193 + } + interface Chainable { + /** + * Wait for a specific event to be sent to amplitude. If the event is found, the subject will be the event. + * + * @param {string} eventName - The type of the event to search for e.g. SwapEventName.SWAP_TRANSACTION_COMPLETED + * @param {number} timeout - The maximum amount of time (in ms) to wait for the event. + * @returns {Chainable} + */ + waitForAmplitudeEvent(eventName: string, requiredProperties?: string[]): Chainable + /** + * Intercepts a specific graphql operation and responds with the given fixture. + * @param {string} operationName - The name of the graphql operation to intercept. + * @param {string} fixturePath - The path to the fixture to respond with. + */ + interceptGraphqlOperation(operationName: string, fixturePath: string): Chainable + /** + * Intercepts a quote request and responds with the given fixture. + * @param {string} fixturePath - The path to the fixture to respond with. + */ + interceptQuoteRequest(fixturePath: string): Chainable + } + interface Cypress { + eagerlyConnect?: boolean + } + interface VisitOptions { + featureFlags?: Array<{ flag: FeatureFlags; value: boolean }> + /** + * Initial user state. + * @default {@type import('../utils/user-state').CONNECTED_WALLET_USER_STATE} + */ + userState?: Partial + /** + * If false, prevents the app from eagerly connecting to the injected provider. + * @default true + */ + eagerlyConnect?: false + } + } +} + +export function registerCommands() { + // sets up the injected provider to be a mock ethereum provider with the given mnemonic/index + // eslint-disable-next-line no-undef + Cypress.Commands.overwrite( + 'visit', + (original, url: string | Partial, options?: Partial) => { + if (typeof url !== 'string') { + throw new Error('Invalid arguments. The first argument to cy.visit must be the path.') + } + + // Parse overrides + const flagsOn: FeatureFlags[] = [] + const flagsOff: FeatureFlags[] = [] + options?.featureFlags?.forEach((f) => { + if (f.value) { + flagsOn.push(f.flag) + } else { + flagsOff.push(f.flag) + } + }) + + // Format into URL parameters + const overrideParams = new URLSearchParams() + if (flagsOn.length > 0) { + overrideParams.append( + 'featureFlagOverride', + flagsOn.map((flag) => getFeatureFlagName(flag, FeatureFlagClient.Web)).join(','), + ) + } + if (flagsOff.length > 0) { + overrideParams.append( + 'featureFlagOverrideOff', + flagsOn.map((flag) => getFeatureFlagName(flag, FeatureFlagClient.Web)).join(','), + ) + } + + return cy.provider().then((provider) => + original({ + ...options, + url: + [...overrideParams.entries()].length === 0 + ? url + : url.includes('?') + ? `${url}&${overrideParams.toString()}` + : `${url}?${overrideParams.toString()}`, + onBeforeLoad(win) { + options?.onBeforeLoad?.(win) + + setInitialUserState(win, { + ...initialState, + ...(options?.userState ?? {}), + }) + + win.ethereum = provider + win.Cypress.eagerlyConnect = options?.eagerlyConnect ?? true + win.localStorage.setItem(ALLOW_ANALYTICS_ATOM_KEY, 'true') + win.localStorage.setItem('showUniswapExtensionLaunchAtom', 'false') + }, + }), + ) + }, + ) + + Cypress.Commands.add('waitForAmplitudeEvent', (eventName, requiredProperties) => { + function findAndDiscardEventsUpToTarget() { + const events = Cypress.env('amplitudeEventCache') + const targetEventIndex = events.findIndex((event) => { + if (event.event_type !== eventName) { + return false + } + if (requiredProperties) { + return requiredProperties.every((prop) => event.event_properties[prop]) + } + return true + }) + + if (targetEventIndex !== -1) { + const event = events[targetEventIndex] + Cypress.env('amplitudeEventCache', events.slice(targetEventIndex + 1)) + return cy.wrap(event) + } else { + // If not found, retry after waiting for more events to be sent. + return cy.wait('@amplitude').then(findAndDiscardEventsUpToTarget) + } + } + return findAndDiscardEventsUpToTarget() + }) + + Cypress.env('graphqlInterceptions', new Map()) + + Cypress.Commands.add('interceptGraphqlOperation', (operationName, fixturePath) => { + const graphqlInterceptions = Cypress.env('graphqlInterceptions') + cy.intercept(/(?:interface|beta).gateway.uniswap.org\/v1\/graphql/, (req) => { + req.headers['origin'] = 'https://app.uniswap.org' + const currentOperationName = req.body.operationName + + if (graphqlInterceptions.has(currentOperationName)) { + const fixturePath = graphqlInterceptions.get(currentOperationName) + req.reply({ fixture: fixturePath }) + } else { + req.continue() + } + }).as(operationName) + + graphqlInterceptions.set(operationName, fixturePath) + }) + + Cypress.Commands.add('interceptQuoteRequest', (fixturePath) => { + return cy.intercept(/(?:interface|beta)\.gateway\.uniswap\.org\/v2\/quote/, (req) => { + req.headers['origin'] = 'https://app.uniswap.org' + req.reply({ fixture: fixturePath }) + }) + }) +} From 085f13dfe805ed43222615349fa29b319583d880 Mon Sep 17 00:00:00 2001 From: AU_019 <64915515+Dargon789@users.noreply.github.com> Date: Sat, 1 Feb 2025 19:32:20 +0700 Subject: [PATCH 11/50] Create SECURITY.md Signed-off-by: AU_019 <64915515+Dargon789@users.noreply.github.com> --- SECURITY.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000000..034e8480320 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,21 @@ +# Security Policy + +## Supported Versions + +Use this section to tell people about which versions of your project are +currently being supported with security updates. + +| Version | Supported | +| ------- | ------------------ | +| 5.1.x | :white_check_mark: | +| 5.0.x | :x: | +| 4.0.x | :white_check_mark: | +| < 4.0 | :x: | + +## Reporting a Vulnerability + +Use this section to tell people how to report a vulnerability. + +Tell them where to go, how often they can expect to get an update on a +reported vulnerability, what to expect if the vulnerability is accepted or +declined, etc. From a6b4970e8ffd383db9de1bdd280ba5b90ed3aeb3 Mon Sep 17 00:00:00 2001 From: AU_019 <64915515+Dargon789@users.noreply.github.com> Date: Sat, 1 Feb 2025 19:43:24 +0700 Subject: [PATCH 12/50] Potential fix for code scanning alert no. 11: Incomplete string escaping or encoding Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: AU_019 <64915515+Dargon789@users.noreply.github.com> --- apps/web/src/components/Logo/DoubleLogo.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/components/Logo/DoubleLogo.tsx b/apps/web/src/components/Logo/DoubleLogo.tsx index 610c4e8205c..9d3dfc98b7e 100644 --- a/apps/web/src/components/Logo/DoubleLogo.tsx +++ b/apps/web/src/components/Logo/DoubleLogo.tsx @@ -47,7 +47,7 @@ function LogolessPlaceholder({ return ( - {currency?.symbol?.toUpperCase().replace('$', '').replace(/\s+/g, '').slice(0, 3)} + {currency?.symbol?.toUpperCase().replace(/\$/g, '').replace(/\s+/g, '').slice(0, 3)} {showNetworkLogo && ( From 8a4ae07b83f18c96635d6dc2d812bcc9753f1b99 Mon Sep 17 00:00:00 2001 From: AU_019 <64915515+Dargon789@users.noreply.github.com> Date: Sat, 25 Jan 2025 03:21:50 +0700 Subject: [PATCH 13/50] Create static.yml Signed-off-by: AU_019 <64915515+Dargon789@users.noreply.github.com> --- .github/workflows/static.yml | 43 ++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 .github/workflows/static.yml diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml new file mode 100644 index 00000000000..f2c9e97c91d --- /dev/null +++ b/.github/workflows/static.yml @@ -0,0 +1,43 @@ +# Simple workflow for deploying static content to GitHub Pages +name: Deploy static content to Pages + +on: + # Runs on pushes targeting the default branch + push: + branches: ["main"] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + # Single deploy job since we're just deploying + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Pages + uses: actions/configure-pages@v5 + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + # Upload entire repository + path: '.' + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 From 46b0af5555e498ccc1ac24f1a954c97741603033 Mon Sep 17 00:00:00 2001 From: AU_019 <64915515+Dargon789@users.noreply.github.com> Date: Sat, 25 Jan 2025 03:17:58 +0700 Subject: [PATCH 14/50] Create jekyll-gh-pages.yml Signed-off-by: AU_019 <64915515+Dargon789@users.noreply.github.com> --- .github/workflows/jekyll-gh-pages.yml | 51 +++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 .github/workflows/jekyll-gh-pages.yml diff --git a/.github/workflows/jekyll-gh-pages.yml b/.github/workflows/jekyll-gh-pages.yml new file mode 100644 index 00000000000..e31d81c5864 --- /dev/null +++ b/.github/workflows/jekyll-gh-pages.yml @@ -0,0 +1,51 @@ +# Sample workflow for building and deploying a Jekyll site to GitHub Pages +name: Deploy Jekyll with GitHub Pages dependencies preinstalled + +on: + # Runs on pushes targeting the default branch + push: + branches: ["main"] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + # Build job + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Pages + uses: actions/configure-pages@v5 + - name: Build with Jekyll + uses: actions/jekyll-build-pages@v1 + with: + source: ./ + destination: ./_site + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + + # Deployment job + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 From bdef85180fee744e0d32cdb3da7085b14302c3cf Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Sat, 15 Feb 2025 21:21:05 +0000 Subject: [PATCH 15/50] fix: packages/ui/package.json to reduce vulnerabilities The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-JS-ELLIPTIC-8720086 - https://snyk.io/vuln/SNYK-JS-ELLIPTIC-8187303 - https://snyk.io/vuln/SNYK-JS-ELLIPTIC-7577916 - https://snyk.io/vuln/SNYK-JS-ELLIPTIC-7577917 - https://snyk.io/vuln/SNYK-JS-ELLIPTIC-7577918 --- packages/ui/package.json | 68 +++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 36 deletions(-) diff --git a/packages/ui/package.json b/packages/ui/package.json index d7f21662fd3..b2be1a7682d 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -2,47 +2,41 @@ "name": "ui", "version": "0.0.0", "dependencies": { - "@gorhom/bottom-sheet": "4.6.4", - "@react-native-masked-view/masked-view": "0.3.2", - "@shopify/flash-list": "1.7.3", - "@shopify/react-native-skia": "1.12.4", - "@storybook/react": "8.5.2", - "@tamagui/animations-react-native": "1.125.17", - "@tamagui/font-inter": "1.125.17", - "@tamagui/helpers-icon": "1.125.17", - "@tamagui/portal": "1.125.17", - "@tamagui/react-native-media-driver": "1.125.17", - "@tamagui/remove-scroll": "1.125.17", - "@tamagui/theme-base": "1.125.17", - "@tanstack/react-query": "5.77.2", - "@testing-library/react-hooks": "8.0.1", - "ethers": "5.7.2", - "expo-blur": "14.0.3", - "expo-linear-gradient": "14.0.2", + "@gorhom/bottom-sheet": "4.5.1", + "@react-native-masked-view/masked-view": "0.2.9", + "@shopify/flash-list": "1.6.3", + "@shopify/react-native-skia": "1.4.2", + "@storybook/react": "8.4.2", + "@tamagui/animations-react-native": "1.114.4", + "@tamagui/font-inter": "1.114.4", + "@tamagui/helpers-icon": "1.114.4", + "@tamagui/react-native-media-driver": "1.114.4", + "@tamagui/remove-scroll": "1.114.4", + "@tamagui/theme-base": "1.114.4", + "@testing-library/react-hooks": "7.0.2", + "ethers": "6.0.0", + "expo-linear-gradient": "12.7.2", "i18next": "23.10.0", "qrcode": "1.5.1", - "react": "18.3.1", - "react-i18next": "14.1.0", - "react-native": "0.77.2", + "react": "18.2.0", + "react-native": "0.73.6", "react-native-fast-image": "8.6.3", - "react-native-gesture-handler": "2.22.1", + "react-native-gesture-handler": "2.19.0", "react-native-image-colors": "1.5.2", - "react-native-keyboard-controller": "1.17.5", - "react-native-reanimated": "3.16.7", - "react-native-safe-area-context": "5.1.0", - "react-native-svg": "15.11.2", - "react-native-webview": "13.13.5", - "tamagui": "1.125.17", + "react-native-reanimated": "3.15.0", + "react-native-safe-area-context": "4.9.0", + "react-native-svg": "15.1.0", + "react-native-webview": "11.23.1", + "tamagui": "1.114.4", "utilities": "workspace:^", "uuid": "9.0.0", "wcag-contrast": "3.0.0" }, "devDependencies": { - "@storybook/test": "8.5.2", - "@tamagui/animations-moti": "1.125.17", - "@tamagui/core": "1.125.17", - "@testing-library/react-native": "13.0.0", - "@types/chrome": "0.0.304", + "@tamagui/animations-moti": "1.114.4", + "@tamagui/build": "1.114.4", + "@tamagui/core": "1.114.4", + "@testing-library/react-native": "11.5.0", "@types/qrcode": "1.5.5", "@uniswap/eslint-config": "workspace:^", "camelcase": "6.3.0", @@ -63,15 +57,17 @@ "module:jsx": "src", "private": true, "scripts": { - "build:icons": "bunx tsx ./src/scripts/componentize-icons.ts && biome check --write --unsafe", - "build:icons:missing": "bun run build:icons --skip-existing", + "build": "tamagui-build --ignore-base-url && node -r esbuild-register ./src/scripts/remove-declaration-files-from-utilities.ts", + "clean": "tamagui-build clean", + "build:icons": "node -r esbuild-register ./src/scripts/componentize-icons.ts", + "build:icons:missing": "yarn build:icons --skip-existing", "check:deps:usage": "depcheck", "lint": "eslint src --max-warnings=0", - "format": "biome check . --linter-enabled=false", + "format": "../../scripts/prettier.sh", "lint:fix": "eslint src --fix", "test": "jest && echo 'ignoring'", "typecheck": "tsc -b", - "watch": "bun run build --watch" + "watch": "yarn build --watch" }, "sideEffects": [ "*.css" From d061a4d1739df56068f7885c85e7e51755b07b9e Mon Sep 17 00:00:00 2001 From: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> Date: Wed, 1 Oct 2025 21:53:05 +0700 Subject: [PATCH 16/50] Update tag_and_release.yml (#68) Refactor the tag_and_release workflow to trigger only on tag pushes, tighten permissions, restructure the release job, and switch from the actions/create-release action to a GitHub CLI invocation. Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> --- .github/workflows/tag_and_release.yml | 30 ++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/.github/workflows/tag_and_release.yml b/.github/workflows/tag_and_release.yml index ec6b8721bc6..f05c7a5689b 100644 --- a/.github/workflows/tag_and_release.yml +++ b/.github/workflows/tag_and_release.yml @@ -33,11 +33,27 @@ jobs: custom_tag: ${{ steps.version.outputs.content }} tag_prefix: "" - - name: 🪽 Release - uses: actions/create-release@8e3b4a5e184b498423d5b25ca5ceb5a9258c92c2 + - name: 🪽 Create release + + on: + push: + tags: + - v* + + permissions: + contents: write + + jobs: + release: + name: Release pushed tag + runs-on: ubuntu-24.04 + steps: + - name: Create release env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Or use your PAT - with: - tag_name: ${{ steps.github-tag-action.outputs.new_tag }} - release_name: Release ${{ steps.github-tag-action.outputs.new_tag }} - body: ${{ steps.release-notes.outputs.content }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + tag: ${{ github.ref_name }} + run: | + gh release create "$tag" \ + --repo="$GITHUB_REPOSITORY" \ + --title="${GITHUB_REPOSITORY#*/} ${tag#v}" \ + --generate-notes From 0b9e0dc6018f9849382563b6f2aa95e74e510272 Mon Sep 17 00:00:00 2001 From: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> Date: Thu, 9 Oct 2025 18:38:42 +0000 Subject: [PATCH 17/50] Create config.yml (#75) Add a CircleCI configuration file with a basic job and workflow CI: Add .circleci/config.yml using CircleCI 2.1 configuration format Define a Docker-based "say-hello" job that checks out the code and prints "Hello, World!" Create "say-hello-workflow" to orchestrate and run the "say-hello" job Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> --- .circleci/config.yml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .circleci/config.yml diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000000..d5d401c5189 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,31 @@ +# Use the latest 2.1 version of CircleCI pipeline process engine. +# See: https://circleci.com/docs/configuration-reference +version: 2.1 + +# Define a job to be invoked later in a workflow. +# See: https://circleci.com/docs/jobs-steps/#jobs-overview & https://circleci.com/docs/configuration-reference/#jobs +jobs: + say-hello: + # Specify the execution environment. You can specify an image from Docker Hub or use one of our convenience images from CircleCI's Developer Hub. + # See: https://circleci.com/docs/executor-intro/ & https://circleci.com/docs/configuration-reference/#executor-job + docker: + # Specify the version you desire here + # See: https://circleci.com/developer/images/image/cimg/base + - image: cimg/base:current + + # Add steps to the job + # See: https://circleci.com/docs/jobs-steps/#steps-overview & https://circleci.com/docs/configuration-reference/#steps + steps: + # Checkout the code as the first step. + - checkout + - run: + name: "Say hello" + command: "echo Hello, World!" + +# Orchestrate jobs using workflows +# See: https://circleci.com/docs/workflows/ & https://circleci.com/docs/configuration-reference/#workflows +workflows: + say-hello-workflow: # This is the name of the workflow, feel free to change it to better match your workflow. + # Inside the workflow, you define the jobs you want to run. + jobs: + - say-hello From 158f5146c430d8c1abaa56f93893adb74b3628c9 Mon Sep 17 00:00:00 2001 From: "snyk-io[bot]" <141718529+snyk-io[bot]@users.noreply.github.com> Date: Thu, 9 Oct 2025 18:16:15 +0000 Subject: [PATCH 18/50] fix: apps/mobile/Gemfile & apps/mobile/Gemfile.lock to reduce vulnerabilities The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-RUBY-REXML-12878608 - https://snyk.io/vuln/SNYK-RUBY-WEBRICK-10500756 - https://snyk.io/vuln/SNYK-RUBY-WEBRICK-8068535 - https://snyk.io/vuln/SNYK-RUBY-REXML-13110060 --- apps/mobile/Gemfile | 4 +- apps/mobile/Gemfile.lock | 184 ++++++++++++++++++++------------------- 2 files changed, 98 insertions(+), 90 deletions(-) diff --git a/apps/mobile/Gemfile b/apps/mobile/Gemfile index a07e6ca69f3..a9e494ea9a1 100644 --- a/apps/mobile/Gemfile +++ b/apps/mobile/Gemfile @@ -1,8 +1,8 @@ source "https://rubygems.org" -gem 'fastlane', '2.214.0' +gem 'fastlane', '2.215.0' # Exclude problematic versions of cocoapods and activesupport that causes build failures. -gem 'cocoapods', '1.14.3' +gem 'cocoapods', '1.15.0' gem 'activesupport', '7.1.2' gem 'xcodeproj', '1.27.0' gem 'concurrent-ruby', '1.3.4' diff --git a/apps/mobile/Gemfile.lock b/apps/mobile/Gemfile.lock index b5891798a1a..1cc3a294c88 100644 --- a/apps/mobile/Gemfile.lock +++ b/apps/mobile/Gemfile.lock @@ -1,7 +1,9 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.6) + CFPropertyList (3.0.7) + base64 + nkf rexml activesupport (7.1.2) base64 @@ -13,37 +15,40 @@ GEM minitest (>= 5.1) mutex_m tzinfo (~> 2.0) - addressable (2.8.6) - public_suffix (>= 2.0.2, < 6.0) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) algoliasearch (1.27.5) httpclient (~> 2.8, >= 2.8.3) json (>= 1.5.1) - artifactory (3.0.15) + artifactory (3.0.17) atomos (0.1.3) - aws-eventstream (1.3.0) - aws-partitions (1.877.0) - aws-sdk-core (3.190.1) + aws-eventstream (1.4.0) + aws-partitions (1.1170.0) + aws-sdk-core (3.233.0) aws-eventstream (~> 1, >= 1.3.0) - aws-partitions (~> 1, >= 1.651.0) - aws-sigv4 (~> 1.8) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) + base64 + bigdecimal jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.75.0) - aws-sdk-core (~> 3, >= 3.188.0) - aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.142.0) - aws-sdk-core (~> 3, >= 3.189.0) + logger + aws-sdk-kms (1.113.0) + aws-sdk-core (~> 3, >= 3.231.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.199.1) + aws-sdk-core (~> 3, >= 3.231.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.8) - aws-sigv4 (1.8.0) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.12.1) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) - base64 (0.2.0) - bigdecimal (3.1.9) + base64 (0.3.0) + bigdecimal (3.3.1) claide (1.1.0) - cocoapods (1.14.3) + cocoapods (1.15.0) addressable (~> 2.8) claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.14.3) + cocoapods-core (= 1.15.0) cocoapods-deintegrate (>= 1.0.3, < 2.0) cocoapods-downloader (>= 2.1, < 3.0) cocoapods-plugins (>= 1.0.0, < 2.0) @@ -58,7 +63,7 @@ GEM nap (~> 1.0) ruby-macho (>= 2.3.0, < 3.0) xcodeproj (>= 1.23.0, < 2.0) - cocoapods-core (1.14.3) + cocoapods-core (1.15.0) activesupport (>= 5.0, < 8) addressable (~> 2.8) algoliasearch (~> 1.0) @@ -82,19 +87,19 @@ GEM commander (4.6.0) highline (~> 2.0.0) concurrent-ruby (1.3.4) - connection_pool (2.5.0) + connection_pool (2.5.4) declarative (0.0.20) - digest-crc (0.6.5) + digest-crc (0.7.0) rake (>= 12.0.0, < 14.0.0) - domain_name (0.6.20231109) + domain_name (0.6.20240107) dotenv (2.8.1) - drb (2.2.1) + drb (2.2.3) emoji_regex (3.2.3) escape (0.0.4) - ethon (0.16.0) + ethon (0.15.0) ffi (>= 1.15.0) - excon (0.109.0) - faraday (1.10.3) + excon (0.112.0) + faraday (1.10.4) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) @@ -110,20 +115,20 @@ GEM faraday (>= 0.8.0) http-cookie (~> 1.0.0) faraday-em_http (1.0.0) - faraday-em_synchrony (1.0.0) + faraday-em_synchrony (1.0.1) faraday-excon (1.1.0) faraday-httpclient (1.0.1) - faraday-multipart (1.0.4) - multipart-post (~> 2) - faraday-net_http (1.0.1) + faraday-multipart (1.1.1) + multipart-post (~> 2.0) + faraday-net_http (1.0.2) faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) faraday-rack (1.0.0) faraday-retry (1.0.3) - faraday_middleware (1.2.0) + faraday_middleware (1.2.1) faraday (~> 1.0) - fastimage (2.3.0) - fastlane (2.214.0) + fastimage (2.4.0) + fastlane (2.215.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -144,6 +149,7 @@ GEM google-apis-playcustomapp_v1 (~> 0.1) google-cloud-storage (~> 1.31) highline (~> 2.0) + http-cookie (~> 1.0.5) json (< 3.0.0) jwt (>= 2.1.0, < 3) mini_magick (>= 4.9.4, < 5.0.0) @@ -155,116 +161,120 @@ GEM security (= 0.1.3) simctl (~> 1.6.3) terminal-notifier (>= 2.0.0, < 3.0.0) - terminal-table (>= 1.4.5, < 2.0.0) + terminal-table (~> 3) tty-screen (>= 0.6.3, < 1.0.0) tty-spinner (>= 0.8.0, < 1.0.0) word_wrap (~> 1.0.0) xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.3.0) xcpretty-travis-formatter (>= 0.0.3) - fastlane-plugin-get_version_name (0.2.2) - fastlane-plugin-versioning_android (0.1.1) - ffi (1.17.1) + ffi (1.17.2) fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.54.0) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-core (0.11.2) + google-apis-androidpublisher_v3 (0.87.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-core (0.18.0) addressable (~> 2.5, >= 2.5.1) - googleauth (>= 0.16.2, < 2.a) - httpclient (>= 2.8.1, < 3.a) + googleauth (~> 1.9) + httpclient (>= 2.8.3, < 3.a) mini_mime (~> 1.0) + mutex_m representable (~> 3.0) retriable (>= 2.0, < 4.a) - rexml - webrick - google-apis-iamcredentials_v1 (0.17.0) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-playcustomapp_v1 (0.13.0) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-storage_v1 (0.29.0) - google-apis-core (>= 0.11.0, < 2.a) - google-cloud-core (1.6.1) + google-apis-iamcredentials_v1 (0.24.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-playcustomapp_v1 (0.17.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-storage_v1 (0.57.0) + google-apis-core (>= 0.15.0, < 2.a) + google-cloud-core (1.8.0) google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) - google-cloud-env (2.1.0) + google-cloud-env (2.3.1) + base64 (~> 0.2) faraday (>= 1.0, < 3.a) - google-cloud-errors (1.3.1) - google-cloud-storage (1.45.0) + google-cloud-errors (1.5.0) + google-cloud-storage (1.57.0) addressable (~> 2.8) digest-crc (~> 0.4) - google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.29.0) + google-apis-core (>= 0.18, < 2) + google-apis-iamcredentials_v1 (~> 0.18) + google-apis-storage_v1 (>= 0.42) google-cloud-core (~> 1.6) - googleauth (>= 0.16.2, < 2.a) + googleauth (~> 1.9) mini_mime (~> 1.0) - googleauth (1.9.1) + google-logging-utils (0.2.0) + googleauth (1.15.0) faraday (>= 1.0, < 3.a) - google-cloud-env (~> 2.1) - jwt (>= 1.4, < 3.0) + google-cloud-env (~> 2.2) + google-logging-utils (~> 0.1) + jwt (>= 1.4, < 4.0) multi_json (~> 1.11) os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) highline (2.0.3) - http-cookie (1.0.5) + http-cookie (1.0.8) domain_name (~> 0.5) - httpclient (2.8.3) + httpclient (2.9.0) + mutex_m i18n (1.14.7) concurrent-ruby (~> 1.0) jmespath (1.6.2) - json (2.7.1) - jwt (2.7.1) - mini_magick (4.12.0) + json (2.15.1) + jwt (2.10.2) + base64 + logger (1.7.0) + mini_magick (4.13.2) mini_mime (1.1.5) - minitest (5.25.4) + minitest (5.26.0) molinillo (0.8.0) - multi_json (1.15.0) - multipart-post (2.3.0) + multi_json (1.17.0) + multipart-post (2.4.1) mutex_m (0.3.0) nanaimo (0.4.0) nap (1.1.0) - naturally (2.2.1) + naturally (2.3.0) netrc (0.11.0) + nkf (0.2.0) optparse (0.1.1) os (1.1.4) - plist (3.7.1) + plist (3.7.2) public_suffix (4.0.7) - rake (13.1.0) + rake (13.3.0) representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.4.1) + rexml (3.4.4) rouge (2.0.7) ruby-macho (2.5.1) ruby2_keywords (0.0.5) - rubyzip (2.3.2) + rubyzip (2.4.1) security (0.1.3) - signet (0.18.0) + signet (0.21.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) - jwt (>= 1.5, < 3.0) + jwt (>= 1.5, < 4.0) multi_json (~> 1.10) simctl (1.6.10) CFPropertyList naturally terminal-notifier (2.0.0) - terminal-table (1.8.0) - unicode-display_width (~> 1.1, >= 1.1.1) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) trailblazer-option (0.1.2) tty-cursor (0.7.1) tty-screen (0.8.2) tty-spinner (0.9.3) tty-cursor (~> 0.7) - typhoeus (1.4.1) - ethon (>= 0.9.0) + typhoeus (1.5.0) + ethon (>= 0.9.0, < 0.16.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) uber (0.1.0) - unicode-display_width (1.8.0) - webrick (1.8.1) + unicode-display_width (2.6.0) word_wrap (1.0.0) xcodeproj (1.27.0) CFPropertyList (>= 2.3.3, < 4.0) @@ -283,12 +293,10 @@ PLATFORMS DEPENDENCIES activesupport (= 7.1.2) - cocoapods (= 1.14.3) + cocoapods (= 1.15.0) concurrent-ruby (= 1.3.4) - fastlane (= 2.214.0) - fastlane-plugin-get_version_name - fastlane-plugin-versioning_android + fastlane (= 2.215.0) xcodeproj (= 1.27.0) BUNDLED WITH - 2.4.10 + 2.3.27 From 1cbe82b0b79b3e6b72732c300f29dd0acb767957 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 9 Oct 2025 18:16:45 +0000 Subject: [PATCH 19/50] build(deps): bump the npm_and_yarn group across 7 directories with 5 updates Bumps the npm_and_yarn group with 2 updates in the /apps/extension directory: [webpack](https://github.com/webpack/webpack) and [webpack-dev-server](https://github.com/webpack/webpack-dev-server). Bumps the npm_and_yarn group with 1 update in the /apps/mobile directory: [react-native-mmkv](https://github.com/mrousavy/react-native-mmkv). Bumps the npm_and_yarn group with 3 updates in the /apps/web directory: [webpack](https://github.com/webpack/webpack), [graphql](https://github.com/graphql/graphql-js) and [hono](https://github.com/honojs/hono). Bumps the npm_and_yarn group with 1 update in the /packages/api directory: [graphql](https://github.com/graphql/graphql-js). Bumps the npm_and_yarn group with 2 updates in the /packages/uniswap directory: [react-native-mmkv](https://github.com/mrousavy/react-native-mmkv) and [graphql](https://github.com/graphql/graphql-js). Bumps the npm_and_yarn group with 1 update in the /packages/utilities directory: [graphql](https://github.com/graphql/graphql-js). Bumps the npm_and_yarn group with 1 update in the /packages/wallet directory: [graphql](https://github.com/graphql/graphql-js). Updates `webpack` from 5.90.0 to 5.94.0 - [Release notes](https://github.com/webpack/webpack/releases) - [Commits](https://github.com/webpack/webpack/compare/v5.90.0...v5.94.0) Updates `webpack-dev-server` from 4.15.1 to 5.2.1 - [Release notes](https://github.com/webpack/webpack-dev-server/releases) - [Changelog](https://github.com/webpack/webpack-dev-server/blob/main/CHANGELOG.md) - [Commits](https://github.com/webpack/webpack-dev-server/compare/v4.15.1...v5.2.1) Updates `react-native-mmkv` from 2.10.1 to 2.11.0 - [Release notes](https://github.com/mrousavy/react-native-mmkv/releases) - [Commits](https://github.com/mrousavy/react-native-mmkv/compare/v2.10.1...v2.11.0) Updates `webpack` from 5.90.0 to 5.94.0 - [Release notes](https://github.com/webpack/webpack/releases) - [Commits](https://github.com/webpack/webpack/compare/v5.90.0...v5.94.0) Updates `graphql` from 16.6.0 to 16.8.1 - [Release notes](https://github.com/graphql/graphql-js/releases) - [Commits](https://github.com/graphql/graphql-js/compare/v16.6.0...v16.8.1) Updates `hono` from 4.8.4 to 4.9.7 - [Release notes](https://github.com/honojs/hono/releases) - [Commits](https://github.com/honojs/hono/compare/v4.8.4...v4.9.7) Updates `graphql` from 16.6.0 to 16.8.1 - [Release notes](https://github.com/graphql/graphql-js/releases) - [Commits](https://github.com/graphql/graphql-js/compare/v16.6.0...v16.8.1) Updates `react-native-mmkv` from 2.10.1 to 2.11.0 - [Release notes](https://github.com/mrousavy/react-native-mmkv/releases) - [Commits](https://github.com/mrousavy/react-native-mmkv/compare/v2.10.1...v2.11.0) Updates `graphql` from 16.6.0 to 16.8.1 - [Release notes](https://github.com/graphql/graphql-js/releases) - [Commits](https://github.com/graphql/graphql-js/compare/v16.6.0...v16.8.1) Updates `graphql` from 16.6.0 to 16.8.1 - [Release notes](https://github.com/graphql/graphql-js/releases) - [Commits](https://github.com/graphql/graphql-js/compare/v16.6.0...v16.8.1) Updates `graphql` from 16.6.0 to 16.8.1 - [Release notes](https://github.com/graphql/graphql-js/releases) - [Commits](https://github.com/graphql/graphql-js/compare/v16.6.0...v16.8.1) --- updated-dependencies: - dependency-name: webpack dependency-version: 5.94.0 dependency-type: direct:development dependency-group: npm_and_yarn - dependency-name: webpack-dev-server dependency-version: 5.2.1 dependency-type: direct:development dependency-group: npm_and_yarn - dependency-name: react-native-mmkv dependency-version: 2.11.0 dependency-type: direct:production dependency-group: npm_and_yarn - dependency-name: webpack dependency-version: 5.94.0 dependency-type: direct:development dependency-group: npm_and_yarn - dependency-name: graphql dependency-version: 16.8.1 dependency-type: direct:production dependency-group: npm_and_yarn - dependency-name: hono dependency-version: 4.9.7 dependency-type: direct:production dependency-group: npm_and_yarn - dependency-name: graphql dependency-version: 16.8.1 dependency-type: direct:production dependency-group: npm_and_yarn - dependency-name: react-native-mmkv dependency-version: 2.11.0 dependency-type: direct:production dependency-group: npm_and_yarn - dependency-name: graphql dependency-version: 16.8.1 dependency-type: direct:production dependency-group: npm_and_yarn - dependency-name: graphql dependency-version: 16.8.1 dependency-type: direct:production dependency-group: npm_and_yarn - dependency-name: graphql dependency-version: 16.8.1 dependency-type: direct:production dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] --- apps/extension/package.json | 4 ++-- apps/mobile/package.json | 2 +- apps/web/package.json | 6 +++--- packages/api/package.json | 2 +- packages/uniswap/package.json | 4 ++-- packages/utilities/package.json | 2 +- packages/wallet/package.json | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/extension/package.json b/apps/extension/package.json index 5a62068364d..ac67f4fe010 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -97,9 +97,9 @@ "swc-loader": "0.2.6", "tamagui-loader": "1.125.17", "typescript": "5.3.3", - "webpack": "5.90.0", + "webpack": "5.94.0", "webpack-cli": "5.1.4", - "webpack-dev-server": "4.15.1" + "webpack-dev-server": "5.2.1" }, "private": true, "scripts": { diff --git a/apps/mobile/package.json b/apps/mobile/package.json index b39462fe118..422cbd68341 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -153,7 +153,7 @@ "react-native-keyboard-controller": "1.17.5", "react-native-localize": "2.2.6", "react-native-markdown-display": "7.0.0-alpha.2", - "react-native-mmkv": "2.10.1", + "react-native-mmkv": "2.11.0", "react-native-onesignal": "5.2.9", "react-native-pager-view": "6.5.1", "react-native-passkey": "3.1.0", diff --git a/apps/web/package.json b/apps/web/package.json index 264da854f4e..757d51e367f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -141,7 +141,7 @@ "vitest": "3.2.1", "vitest-fetch-mock": "0.4.5", "wait-on": "8.0.2", - "webpack": "5.90.0", + "webpack": "5.94.0", "wrangler": "4.28.0" }, "dependencies": { @@ -213,8 +213,8 @@ "ethers": "5.7.2", "fancy-canvas": "2.1.0", "framer-motion": "10.17.6", - "graphql": "16.6.0", - "hono": "4.8.4", + "graphql": "16.8.1", + "hono": "4.9.7", "html-entities": "2.6.0", "i18next": "23.10.0", "jotai": "1.3.7", diff --git a/packages/api/package.json b/packages/api/package.json index 1f537020b17..40f39ddccc2 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -8,7 +8,7 @@ "@universe/config": "workspace:^", "@universe/sessions": "workspace:^", "expo-secure-store": "14.0.1", - "graphql": "16.6.0", + "graphql": "16.8.1", "react": "18.3.1", "utilities": "workspace:^" }, diff --git a/packages/uniswap/package.json b/packages/uniswap/package.json index f4db7bdb1b3..fa142a1b1fd 100644 --- a/packages/uniswap/package.json +++ b/packages/uniswap/package.json @@ -75,7 +75,7 @@ "expo-haptics": "14.0.1", "expo-web-browser": "14.0.2", "fuse.js": "6.5.3", - "graphql": "16.6.0", + "graphql": "16.8.1", "i18next": "23.10.0", "i18next-resources-to-backend": "1.2.1", "idb-keyval": "6.2.1", @@ -95,7 +95,7 @@ "react-native-fast-image": "8.6.3", "react-native-gesture-handler": "2.22.1", "react-native-localize": "2.2.6", - "react-native-mmkv": "2.10.1", + "react-native-mmkv": "2.11.0", "react-native-reanimated": "3.16.7", "react-native-svg": "15.11.2", "react-native-webview": "13.13.5", diff --git a/packages/utilities/package.json b/packages/utilities/package.json index 94ce33a3b3f..116f59393d3 100644 --- a/packages/utilities/package.json +++ b/packages/utilities/package.json @@ -24,7 +24,7 @@ "@uniswap/sdk-core": "7.7.2", "dayjs": "1.11.7", "expo-localization": "16.0.1", - "graphql": "16.6.0", + "graphql": "16.8.1", "jsbi": "3.2.5", "promise": "8.3.0", "react": "18.3.1", diff --git a/packages/wallet/package.json b/packages/wallet/package.json index 3e0634f44ef..c84d751abf2 100644 --- a/packages/wallet/package.json +++ b/packages/wallet/package.json @@ -36,7 +36,7 @@ "dayjs": "1.11.7", "ethers": "5.7.2", "fuse.js": "6.5.3", - "graphql": "16.6.0", + "graphql": "16.8.1", "i18next": "23.10.0", "jsbi": "3.2.5", "lodash": "4.17.21", From 9ec565f326ffce0392b3d907c2f82dab21facee0 Mon Sep 17 00:00:00 2001 From: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> Date: Fri, 10 Oct 2025 02:04:35 +0700 Subject: [PATCH 20/50] Create notify vercel.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> --- .github/workflows/notify vercel.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .github/workflows/notify vercel.yml diff --git a/.github/workflows/notify vercel.yml b/.github/workflows/notify vercel.yml new file mode 100644 index 00000000000..ae9288a9d30 --- /dev/null +++ b/.github/workflows/notify vercel.yml @@ -0,0 +1,4 @@ +- name: 'notify vercel' + uses: 'vercel/repository-dispatch/actions/status@v1' + with: + name: Vercel - interface-web: notify vercel From 6fec7e2a8bc47755cbaed7c0c9e6d4621ae30ca9 Mon Sep 17 00:00:00 2001 From: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> Date: Fri, 10 Oct 2025 03:02:52 +0700 Subject: [PATCH 21/50] Delete .github/workflows/notify vercel.yml Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> --- .github/workflows/notify vercel.yml | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 .github/workflows/notify vercel.yml diff --git a/.github/workflows/notify vercel.yml b/.github/workflows/notify vercel.yml deleted file mode 100644 index ae9288a9d30..00000000000 --- a/.github/workflows/notify vercel.yml +++ /dev/null @@ -1,4 +0,0 @@ -- name: 'notify vercel' - uses: 'vercel/repository-dispatch/actions/status@v1' - with: - name: Vercel - interface-web: notify vercel From 3f6d466632146384b383f9f1ee521ddfcdbecf20 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Oct 2025 19:04:58 +0000 Subject: [PATCH 22/50] build(deps-dev): bump playwright Bumps the npm_and_yarn group with 1 update in the /apps/web directory: [playwright](https://github.com/microsoft/playwright). Updates `playwright` from 1.49.1 to 1.55.1 - [Release notes](https://github.com/microsoft/playwright/releases) - [Commits](https://github.com/microsoft/playwright/compare/v1.49.1...v1.55.1) --- updated-dependencies: - dependency-name: playwright dependency-version: 1.55.1 dependency-type: direct:development dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] --- apps/web/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/package.json b/apps/web/package.json index 757d51e367f..4009a64efb4 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -116,7 +116,7 @@ "jest-styled-components": "7.2.0", "lint-staged": "14.0.1", "madge": "6.1.0", - "playwright": "1.49.1", + "playwright": "1.55.1", "postinstall-postinstall": "2.1.0", "process": "0.11.10", "prop-types": "15.8.1", From 8d28ba55d28236c8c6afe8aac85e1392c0925173 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Oct 2025 23:40:39 +0000 Subject: [PATCH 23/50] build(deps): bump hono Bumps the npm_and_yarn group with 1 update in the /apps/web directory: [hono](https://github.com/honojs/hono). Updates `hono` from 4.9.7 to 4.10.3 - [Release notes](https://github.com/honojs/hono/releases) - [Commits](https://github.com/honojs/hono/compare/v4.9.7...v4.10.3) --- updated-dependencies: - dependency-name: hono dependency-version: 4.10.3 dependency-type: direct:production dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] --- apps/web/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/package.json b/apps/web/package.json index 4009a64efb4..adf5afbb5bc 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -214,7 +214,7 @@ "fancy-canvas": "2.1.0", "framer-motion": "10.17.6", "graphql": "16.8.1", - "hono": "4.9.7", + "hono": "4.10.3", "html-entities": "2.6.0", "i18next": "23.10.0", "jotai": "1.3.7", From 7f595cd180db8a7eeac4f0a7c14be589439efe1e Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Sun, 26 Oct 2025 15:54:12 +0000 Subject: [PATCH 24/50] Create docker.yml (#87) Add a new GitHub Actions workflow under .circleci/docker.yml to automate Docker image building, pushing, and signing with Buildx, caching, and cosign on scheduled and event-driven triggers New Features: Add Docker GitHub Actions workflow to build and push container images to the registry Integrate cosign to sign published images outside of pull requests Enhancements: Use Docker Buildx for multi-platform builds with GitHub Actions cache Extract and apply Docker metadata (tags and labels) via docker/metadata-action CI: Trigger the Docker workflow on a daily cron, master branch pushes, pull requests, and semver tag creations Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --- .circleci/docker.yml | 100 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 .circleci/docker.yml diff --git a/.circleci/docker.yml b/.circleci/docker.yml new file mode 100644 index 00000000000..e994f94e708 --- /dev/null +++ b/.circleci/docker.yml @@ -0,0 +1,100 @@ +name: Docker + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +on: + schedule: + - cron: '21 12 * * *' + push: + branches: [ "master" ] + # Publish semver tags as releases. + tags: [ 'v*.*.*' ] + pull_request: + branches: [ "master" ] + +env: + # Use docker.io for Docker Hub if empty + REGISTRY: ghcr.io + # github.repository as / + IMAGE_NAME: ${{ github.repository }} + + +jobs: + build: + - name: Build the Docker image + run: docker build . --file path/to/Dockerfile --tag my-image-name:$(date +%s) + + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + # This is used to complete the identity challenge + # with sigstore/fulcio when running outside of PRs. + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Install the cosign tool except on PR + # https://github.com/sigstore/cosign-installer + - name: Install cosign + if: github.event_name != 'pull_request' + uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 #v3.5.0 + with: + cosign-release: 'v2.2.4' + + # Set up BuildKit Docker container builder to be able to build + # multi-platform images and export cache + # https://github.com/docker/setup-buildx-action + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 + + # Login against a Docker registry except on PR + # https://github.com/docker/login-action + - name: Log into registry ${{ env.REGISTRY }} + if: github.event_name != 'pull_request' + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Extract metadata (tags, labels) for Docker + # https://github.com/docker/metadata-action + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + # Build and push Docker image with Buildx (don't push on PR) + # https://github.com/docker/build-push-action + - name: Build and push Docker image + id: build-and-push + uses: docker/build-push-action@v5.0.0 + with: + context: ./ + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + # Sign the resulting Docker image digest except on PRs. + # This will only write to the public Rekor transparency log when the Docker + # repository is public to avoid leaking data. If you would like to publish + # transparency data even for private images, pass --force to cosign below. + # https://github.com/sigstore/cosign + - name: Sign the published Docker image + if: ${{ github.event_name != 'pull_request' }} + env: + # https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable + TAGS: ${{ steps.meta.outputs.tags }} + DIGEST: ${{ steps.build-and-push.outputs.digest }} + # This step uses the identity token to provision an ephemeral certificate + # against the sigstore community Fulcio instance. + run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST} From 557b3bd02c5ccc6df9fe4c93e80447ac3879ee88 Mon Sep 17 00:00:00 2001 From: Uniswap Labs Service Account Date: Tue, 28 Oct 2025 19:55:37 +0000 Subject: [PATCH 25/50] ci(release): publish latest release --- .gitignore | 7 +- RELEASE | 52 +- VERSION | 2 +- apps/extension/package.json | 1 + apps/extension/src/app/apollo.tsx | 10 +- .../src/app/components/AutoLockProvider.tsx | 3 +- .../src/app/core/StatsigProvider.tsx | 3 +- .../app/core/initStatSigForBrowserScripts.tsx | 3 +- .../useShouldShowBiometricUnlock.ts | 3 +- .../useShouldShowBiometricUnlockEnrollment.ts | 3 +- .../SignTypedDataRequestContent.tsx | 3 +- .../src/app/features/dappRequests/saga.ts | 3 +- .../src/app/features/home/HomeScreen.tsx | 3 +- .../features/home/PortfolioActionButtons.tsx | 3 +- .../src/app/features/home/PortfolioHeader.tsx | 2 +- .../onboarding/import/SelectWallets.tsx | 3 +- .../onboarding/scan/ScanToOnboard.tsx | 2 +- .../app/features/settings/SettingsScreen.tsx | 3 +- .../useIsExtensionPasskeyImportEnabled.ts | 3 +- .../src/entrypoints/sidepanel/main.tsx | 3 + apps/extension/tsconfig.json | 3 + .../deeplinks/deeplink-comprehensive.yaml | 128 +- .../flows/restore/restore-new-device.yaml | 21 +- .../shared-flows/navigate-to-explore.yaml | 23 +- apps/mobile/package.json | 1 + apps/mobile/src/app/App.tsx | 25 +- .../app/MobileWalletNavigationProvider.tsx | 3 +- .../app/modals/BridgedAssetWarningWrapper.tsx | 3 +- apps/mobile/src/app/navigation/navigation.tsx | 3 +- .../tabs/CustomTabBar/CustomTabBar.tsx | 1 + .../navigation/tabs/SwapLongPressModal.tsx | 3 +- .../PriceExplorer/PriceExplorer.tsx | 19 +- .../src/components/PriceExplorer/Text.tsx | 43 +- .../__snapshots__/Text.test.tsx.snap | 9 +- .../components/PriceExplorer/useFiatDelta.tsx | 116 ++ .../src/components/PriceExplorer/usePrice.tsx | 2 +- .../PriceExplorer/usePriceHistory.ts | 2 +- .../ModalWithOverlay/ModalWithOverlay.tsx | 3 +- .../WalletConnectRequestModal.tsx | 3 +- .../Requests/ScanSheet/WalletConnectModal.tsx | 3 +- .../Requests/ScanSheet/util.test.ts | 84 ++ .../src/components/Requests/ScanSheet/util.ts | 29 +- .../src/components/Requests/Uwulink/utils.ts | 14 +- .../TokenDetailsBridgedAssetSection.tsx | 4 +- .../src/components/accounts/AccountHeader.tsx | 5 +- .../components/activity/ActivityContent.tsx | 11 +- .../src/components/carousel/Carousel.tsx | 4 +- .../ExploreSections/ExploreSections.tsx | 3 +- .../search/ExploreScreenSearchResultsList.tsx | 3 +- .../src/components/home/HomeExploreTab.tsx | 3 +- apps/mobile/src/components/home/TokensTab.tsx | 3 +- apps/mobile/src/components/home/hooks.tsx | 17 +- .../home/introCards/FundWalletModal.tsx | 3 +- .../introCards/OnboardingIntroCardStack.tsx | 3 +- .../src/components/layout/TabHelpers.tsx | 3 +- .../components/loading/parts/WaveLoader.tsx | 1 - .../datadog/DatadogProviderWrapper.tsx | 10 +- .../src/features/deepLinking/configUtils.ts | 4 +- .../src/features/deepLinking/deepLinkUtils.ts | 2 +- .../deepLinking/handleDeepLinkSaga.test.ts | 6 +- .../deepLinking/handleDeepLinkSaga.ts | 3 +- .../deepLinking/handleOnRampReturnLinkSaga.ts | 3 +- .../deepLinking/handleTransactionLinkSaga.ts | 3 +- .../features/lockScreen/LockScreenModal.tsx | 3 +- .../src/features/send/SendFormButton.tsx | 3 +- .../src/features/wallet/useWalletRestore.ts | 3 +- .../walletConnect/batchedTransactionSaga.ts | 3 +- .../mobile/src/features/walletConnect/saga.ts | 3 +- apps/mobile/src/screens/ActivityScreen.tsx | 11 +- apps/mobile/src/screens/AppLoadingScreen.tsx | 3 +- apps/mobile/src/screens/ExploreScreen.tsx | 5 +- .../src/screens/HomeScreen/HomeScreen.tsx | 3 +- .../HomeScreen/HomeScreenQuickActions.tsx | 3 +- .../src/screens/Import/ImportMethodScreen.tsx | 3 +- .../screens/Import/OnDeviceRecoveryScreen.tsx | 3 +- .../screens/Import/RestoreMethodScreen.tsx | 3 +- .../src/screens/Import/SelectWalletScreen.tsx | 3 +- .../src/screens/Onboarding/LandingScreen.tsx | 5 +- apps/mobile/src/screens/SettingsScreen.tsx | 3 +- .../mobile/src/screens/TokenDetailsScreen.tsx | 9 +- .../ViewPrivateKeys/ViewPrivateKeysScreen.tsx | 3 +- apps/mobile/tsconfig.json | 3 + apps/web/.eslintrc.js | 26 + apps/web/package.json | 3 +- apps/web/project.json | 4 +- .../images/portfolio_page_promo/dark.png | Bin 0 -> 232460 bytes .../images/portfolio_page_promo/light.png | Bin 0 -> 279063 bytes apps/web/public/pools-sitemap.xml | 20 + apps/web/public/tokens-sitemap.xml | 90 ++ apps/web/src/appGraphql/data/util.tsx | 6 + .../images/portfolio-page-promo/dark.svg | 778 +++++++++++ .../images/portfolio-page-promo/light.svg | 778 +++++++++++ apps/web/src/assets/svg/Emblem/A.svg | 4 + apps/web/src/assets/svg/Emblem/B.svg | 3 + apps/web/src/assets/svg/Emblem/C.svg | 18 + apps/web/src/assets/svg/Emblem/D.svg | 4 + apps/web/src/assets/svg/Emblem/E.svg | 3 + apps/web/src/assets/svg/Emblem/F.svg | 3 + apps/web/src/assets/svg/Emblem/G.svg | 3 + apps/web/src/assets/svg/Emblem/default.svg | 3 + .../AccountDrawer/AuthenticatedHeader.tsx | 3 +- .../AccountDrawer/DisconnectButton.tsx | 3 +- .../Activity/ActivityTab.anvil.e2e.test.ts | 1 + .../MiniPortfolio/ExtensionDeeplinks.tsx | 2 +- .../MiniPortfolio/Pools/PoolsTab.tsx | 2 +- .../MiniPortfolio/PortfolioRow.tsx | 60 +- .../ActivityTable/ActivityAddressCell.tsx | 27 + .../ActivityTable/ActivityAmountCell.tsx | 255 ++++ .../ActivityTable/ActivityTable.tsx | 123 ++ .../ActivityTable/AddressWithAvatar.tsx | 42 + .../src/components/ActivityTable/TimeCell.tsx | 15 + .../ActivityTable/TokenAmountDisplay.tsx | 32 + .../ActivityTable/TransactionTypeCell.tsx | 30 + .../ActivityTable/activityTableModels.ts | 61 + .../src/components/ActivityTable/registry.ts | 246 ++++ .../BridgingPopularTokensBanner.tsx | 108 ++ .../src/components/Banner/shared/Banners.tsx | 13 +- .../D3LiquidityRangeChart.tsx | 10 +- .../components/D3LiquidityMinMaxInput.tsx | 17 +- .../D3LiquidityRangeChart/store/types.ts | 2 +- .../D3LiquidityRangeChart/utils/tickUtils.ts | 2 +- .../D3LiquidityRangeInput.tsx | 7 +- .../Charts/LiquidityChart/index.tsx | 2 +- .../LiquidityPositionRangeChart.tsx | 2 +- .../LiquidityRangeInput.tsx | 2 +- .../Charts/LiquidityRangeInput/hooks.ts | 2 +- .../components/Charts/PriceChart/index.tsx | 66 +- apps/web/src/components/Expand/index.tsx | 59 +- .../FeatureFlagModal/FeatureFlagModal.tsx | 24 +- .../src/components/HelpModal/HelpContent.tsx | 3 +- .../src/components/HelpModal/HelpModal.tsx | 26 +- .../components/Liquidity/ClaimFeeModal.tsx | 2 +- .../components/Liquidity/Create/AddHook.tsx | 2 +- .../components/Liquidity/Create/EditStep.tsx | 2 +- .../Liquidity/Create/FormWrapper.tsx | 2 +- .../Create/PositionOutOfRangeError.tsx | 2 +- .../Liquidity/Create/RangeSelectionStep.tsx | 6 +- .../Liquidity/Create/SelectTokenStep.tsx | 16 +- .../Create/hooks/useDepositInfo.test.ts | 6 +- .../Liquidity/Create/hooks/useDepositInfo.tsx | 2 +- .../hooks/useDerivedPositionInfo.test.ts | 8 +- .../Create/hooks/useDerivedPositionInfo.tsx | 5 +- .../Create/hooks/useLPSlippageValues.test.ts | 6 +- .../Create/hooks/useLPSlippageValues.ts | 5 +- ...seNativeTokenPercentageBufferExperiment.ts | 3 +- .../src/components/Liquidity/Create/types.ts | 14 +- apps/web/src/components/Liquidity/Deposit.tsx | 2 +- .../Liquidity/DisplayCurrentPrice.tsx | 3 +- .../Liquidity/FeeTierSearchModal.tsx | 3 +- .../Liquidity/LiquidityPositionCard.tsx | 5 +- .../Liquidity/LiquidityPositionFeeStats.tsx | 2 +- .../Liquidity/LiquidityPositionInfo.test.tsx | 2 +- .../Liquidity/LiquidityPositionInfo.tsx | 2 +- .../LiquidityPositionInfoBadges.test.tsx | 2 +- .../Liquidity/LiquidityPositionInfoBadges.tsx | 2 +- .../LiquidityPositionStatusIndicator.test.tsx | 2 +- .../LiquidityPositionStatusIndicator.tsx | 2 +- .../Liquidity/PositionPageActionButtons.tsx | 2 +- .../components/Liquidity/PositionsHeader.tsx | 2 +- .../src/components/Liquidity/TokenInfo.tsx | 2 +- .../web/src/components/Liquidity/analytics.ts | 2 +- .../web/src/components/Liquidity/constants.ts | 2 +- .../hooks/useAllFeeTierPoolData.test.tsx | 2 +- .../Liquidity/hooks/useAllFeeTierPoolData.ts | 2 +- apps/web/src/components/Liquidity/types.ts | 2 +- .../Liquidity/utils/currency.test.ts | 2 +- .../components/Liquidity/utils/currency.ts | 2 +- .../Liquidity/utils/feeTiers.test.ts | 2 +- .../components/Liquidity/utils/feeTiers.ts | 2 +- ...etPoolIdOrAddressFromCreatePositionInfo.ts | 2 +- .../Liquidity/utils/getPositionUrl.test.ts | 2 +- .../Liquidity/utils/getPositionUrl.ts | 2 +- .../utils/hasLPFoTTransferError.test.ts | 2 +- .../Liquidity/utils/hasLPFoTTransferError.ts | 2 +- .../Liquidity/utils/parseFromRest.test.ts | 2 +- .../Liquidity/utils/parseFromRest.ts | 2 +- .../Liquidity/utils/priceRangeInfo.test.ts | 2 +- .../Liquidity/utils/priceRangeInfo.ts | 2 +- .../Liquidity/utils/protocolVersion.test.ts | 2 +- .../Liquidity/utils/protocolVersion.ts | 2 +- .../NavBar/CompanyMenu/MenuDropdown.tsx | 27 +- .../NavBar/CompanyMenu/MobileMenuDrawer.tsx | 22 +- .../components/NavBar/CompanyMenu/index.tsx | 2 +- .../NavBar/DownloadApp/Modal/GetStarted.tsx | 3 +- .../NavBar/DownloadApp/Modal/index.tsx | 3 +- .../DownloadApp/NewUserCTAButton.test.tsx | 11 +- .../NavBar/DownloadApp/NewUserCTAButton.tsx | 3 +- .../NavBar/LegalAndPrivacyMenu/index.tsx | 6 +- .../NavBar/NavDropdown/NavDropdown.tsx | 7 +- .../NavBar/SearchBar/SearchModal.tsx | 3 +- .../src/components/NavBar/SearchBar/index.tsx | 3 +- .../NavBar/SearchBar/useIsSearchBarVisible.ts | 3 +- .../components/NavBar/Tabs/TabsContent.tsx | 3 +- apps/web/src/components/NavBar/index.tsx | 3 +- .../Pools/PoolDetails/ChartSection/index.tsx | 4 +- .../Pools/PoolDetails/PoolDetailsHeader.tsx | 3 +- .../PoolDetails/PoolDetailsStatsButtons.tsx | 2 +- .../components/Pools/PoolTable/PoolTable.tsx | 3 +- apps/web/src/components/Popups/PopupItem.tsx | 1 + .../components/Popups/ToastRegularSimple.tsx | 6 +- .../src/components/StatusIcon/index.test.tsx | 11 +- apps/web/src/components/Table/index.tsx | 72 +- .../Tokens/TokenDetails/BalanceSummary.tsx | 7 +- .../TokenDetails/BridgedAssetSection.tsx | 3 +- .../components/Tokens/TokenDetails/Delta.tsx | 26 +- .../__snapshots__/Delta.test.tsx.snap | 10 +- .../components/Tokens/TokenDetails/index.tsx | 2 +- .../WalletModal/DownloadWalletOption.tsx | 3 +- .../WalletModal/UniswapWalletOptions.test.tsx | 30 +- .../WalletModal/WalletConnectorOption.tsx | 3 +- .../UniswapWalletOptions.test.tsx.snap | 4 +- apps/web/src/components/WalletModal/index.tsx | 3 +- .../Web3Provider/WebUniswapContext.tsx | 3 +- .../Web3Provider/rejectableConnector.ts | 52 + .../Web3Provider/wagmiAutoConnect.ts | 4 +- .../components/Web3Provider/wagmiConfig.ts | 5 +- .../Web3Status/RecentlyConnectedModal.tsx | 3 +- apps/web/src/components/Web3Status/index.tsx | 3 +- apps/web/src/dev/DevFlagsBox.tsx | 5 +- .../src/featureFlags/flags/outageBanner.ts | 3 +- .../useFeatureFlagUrlOverrides.tsx | 3 +- .../features/accounts/store/provider.test.tsx | 8 +- .../src/features/accounts/store/provider.tsx | 3 +- .../src/features/accounts/store/updater.tsx | 3 +- .../connection/connectors/solana.test.ts | 3 +- .../hooks/useOrderedWalletConnectors.test.ts | 14 +- .../hooks/useOrderedWalletConnectors.ts | 3 +- .../providers/ExternalWalletProvider.tsx | 3 +- apps/web/src/hooks/useConfirmModalState.ts | 20 +- .../hooks/useIsUniswapExtensionConnected.ts | 3 +- .../src/hooks/useIsUniswapXSupportedChain.ts | 3 +- .../hooks/useLpIncentivesFormattedEarnings.ts | 5 +- apps/web/src/hooks/usePoolTickData.ts | 2 +- apps/web/src/hooks/usePositionTokenURI.ts | 2 +- apps/web/src/index.tsx | 5 +- .../hooks/routing/useRoutingAPIArguments.ts | 3 +- apps/web/src/lib/utils/analytics.ts | 8 +- .../pages/App/WalletConnection.e2e.test.ts | 2 +- .../CreateLiquidityContextProvider.tsx | 5 +- .../CreatePosition.anvil.e2e.test.ts | 7 +- .../CreatePosition/CreatePosition.e2e.test.ts | 8 +- .../pages/CreatePosition/CreatePosition.tsx | 2 +- .../CreatePosition/CreatePositionModal.tsx | 2 +- .../CreatePositionTxContext.test.ts | 2 +- .../CreatePositionTxContext.tsx | 2 +- apps/web/src/pages/Errors.anvil.e2e.test.ts | 18 +- apps/web/src/pages/Explore/ProtocolFilter.tsx | 2 +- apps/web/src/pages/Explore/redirects.tsx | 3 +- .../useExternallyConnectableExtensionId.ts | 3 +- .../IncreaseLiquidity.anvil.e2e.test.ts | 2 +- .../IncreaseLiquidityForm.tsx | 2 +- .../IncreaseLiquidityReview.tsx | 2 +- .../IncreaseLiquidityTxContext.tsx | 2 +- .../hooks/useDerivedIncreaseLiquidityInfo.ts | 2 +- .../web/src/pages/Landing/sections/Footer.tsx | 6 +- apps/web/src/pages/Landing/sections/Hero.tsx | 2 +- apps/web/src/pages/LegacyPool/redirects.tsx | 2 +- .../web/src/pages/MigrateV2/MigrateV2Pair.tsx | 2 +- .../MigrateV3/MigrateV3.anvil.e2e.test.ts | 2 +- .../src/pages/MigrateV3/MigrateV3.e2e.test.ts | 2 +- .../MigrateV3/MigrateV3LiquidityTxContext.tsx | 2 +- .../MigrateV3/hooks/useInitialPosition.ts | 2 +- apps/web/src/pages/MigrateV3/index.tsx | 2 +- apps/web/src/pages/PoolDetails/index.tsx | 3 +- .../src/pages/Portfolio/Activity/Activity.tsx | 137 +- .../pages/Portfolio/Activity/Filters/utils.ts | 104 +- .../pages/Portfolio/ConnectWalletBanner.tsx | 124 ++ .../Portfolio/ConnectWalletBottomOverlay.tsx | 47 + .../src/pages/Portfolio/ConnectWalletView.tsx | 41 - .../web/src/pages/Portfolio/Header/Header.tsx | 30 +- .../ConnectedAddressDisplay.tsx | 39 + .../DemoAddressDisplay.tsx | 37 + .../PortfolioAddressDisplay.tsx | 9 + .../Portfolio/Header/hooks/useIsConnected.ts | 7 + apps/web/src/pages/Portfolio/NFTs/NFTCard.tsx | 144 ++ apps/web/src/pages/Portfolio/NFTs/Nfts.tsx | 75 + .../Portfolio/NFTs/utils/filterNfts.test.ts | 257 ++++ .../pages/Portfolio/NFTs/utils/filterNfts.ts | 32 + apps/web/src/pages/Portfolio/Nfts.tsx | 30 - apps/web/src/pages/Portfolio/Portfolio.tsx | 101 +- .../src/pages/Portfolio/PortfolioContent.tsx | 42 + .../Portfolio/PortfolioDisconnectedView.tsx | 105 ++ .../Tokens/Table/TokensContextMenuWrapper.tsx | 4 +- .../Portfolio/Tokens/Table/TokensTable.tsx | 56 + .../Table/{Table.tsx => TokensTableInner.tsx} | 73 +- .../Tokens/Table/columns/Balance.tsx | 2 +- .../web/src/pages/Portfolio/Tokens/Tokens.tsx | 121 +- .../hooks/useTransformTokenTableData.ts | 74 +- .../Tokens/utils/filterTokensBySearch.test.ts | 280 ++++ .../Tokens/utils/filterTokensBySearch.ts | 28 + .../Portfolio/hooks/usePortfolioAddress.ts | 13 + .../Positions/ClaimFees.anvil.e2e.test.ts | 2 +- apps/web/src/pages/Positions/PositionPage.tsx | 5 +- apps/web/src/pages/Positions/TopPools.tsx | 3 +- .../src/pages/Positions/V2PositionPage.tsx | 5 +- apps/web/src/pages/Positions/index.tsx | 5 +- .../RemoveLiquidity.anvil.e2e.test.ts | 2 +- .../RemoveLiquidityModalContext.tsx | 2 +- .../RemoveLiquidity/RemoveLiquidityReview.tsx | 2 +- .../hooks/useRemoveLiquidityTxAndGasInfo.ts | 2 +- apps/web/src/pages/RouteDefinitions.tsx | 3 +- apps/web/src/pages/Swap/Buy/Buy.e2e.test.ts | 3 + apps/web/src/pages/Swap/Buy/hooks.ts | 3 +- .../web/src/pages/Swap/Fees.anvil.e2e.test.ts | 1 + apps/web/src/pages/Swap/Fees.e2e.test.ts | 4 +- apps/web/src/pages/Swap/Limit/LimitForm.tsx | 3 +- .../src/pages/Swap/Logging.anvil.e2e.test.ts | 1 + .../Swap/Send/NewAddressSpeedBump.test.tsx | 3 +- apps/web/src/pages/Swap/Send/SendForm.tsx | 3 +- .../web/src/pages/Swap/Swap.anvil.e2e.test.ts | 22 +- apps/web/src/pages/Swap/index.tsx | 46 +- .../pages/Swap/settings/useWebSwapSettings.ts | 3 +- .../web/src/playwright/anvil/anvil-manager.ts | 4 +- apps/web/src/playwright/fixtures/anvil.ts | 13 +- .../web/src/playwright/fixtures/tradingApi.ts | 12 +- apps/web/src/setupTests.ts | 7 +- .../state/activity/polling/transactions.ts | 3 +- .../src/state/explore/protocolStats.test.tsx | 9 +- apps/web/src/state/explore/topPools.ts | 2 +- apps/web/src/state/limit/hooks.ts | 3 +- .../state/routing/useRoutingAPITrade.test.ts | 3 +- .../state/sagas/liquidity/liquiditySaga.ts | 3 +- apps/web/src/state/sagas/root.ts | 2 + apps/web/src/state/sagas/transactions/5792.ts | 4 +- .../src/state/sagas/transactions/solana.ts | 72 +- .../src/state/sagas/transactions/swapSaga.ts | 104 +- .../src/state/sagas/transactions/uniswapx.ts | 2 +- .../web/src/state/sagas/transactions/utils.ts | 45 +- .../src/state/sagas/transactions/wrapSaga.ts | 4 +- apps/web/src/state/swap/hooks.test.tsx | 114 +- apps/web/src/state/swap/hooks.tsx | 25 +- apps/web/src/state/transactions/types.ts | 6 +- .../hooks/useMismatchAccount.ts | 3 +- .../hooks/useWalletGetCapabilitiesMutation.ts | 3 +- .../src/utils/computeSurroundingTicks.test.ts | 2 +- apps/web/src/utils/computeSurroundingTicks.ts | 2 +- apps/web/tsconfig.json | 3 + apps/web/vite.config.mts | 2 +- bun.lock | 300 ++-- config/jest-presets/jest/jest-preset.js | 2 +- config/jest-presets/jest/setup.js | 54 +- config/vitest-presets/vitest/vitest-preset.js | 1 - dangerfile.ts | 2 +- nx.json | 30 +- package.json | 41 +- packages/api/package.json | 3 +- .../api/src/clients/graphql/queries.graphql | 6 + .../api/src/clients/graphql/schema.graphql | 59 +- .../createNotificationsApiClient.ts | 49 + .../api/src/clients/notifications/types.ts | 38 + .../api/src/clients/trading/tradeTypes.ts | 4 +- packages/api/src/connectRpc/utils.ts | 2 +- packages/api/src/index.ts | 12 +- packages/biome-config/base.jsonc | 12 +- packages/gating/.eslintrc.js | 46 + packages/gating/README.md | 3 + packages/gating/package.json | 31 + packages/gating/project.json | 16 + .../src}/LocalOverrideAdapterWrapper.ts | 2 +- .../features/gating => gating/src}/configs.ts | 61 +- .../gating => gating/src}/constants.ts | 0 .../gating => gating/src}/experiments.ts | 10 + .../features/gating => gating/src}/flags.ts | 2 + .../src}/getStatsigEnvName.ts | 0 .../features/gating => gating/src}/hooks.ts | 10 +- packages/gating/src/index.ts | 86 ++ .../src}/sdk/statsig.native.ts | 6 +- .../gating => gating/src}/sdk/statsig.ts | 2 +- .../features/gating => gating/src}/utils.ts | 2 +- packages/gating/tsconfig.json | 19 + packages/gating/tsconfig.lint.json | 8 + packages/notifications/.eslintrc.js | 45 + packages/notifications/README.md | 73 + packages/notifications/package.json | 18 + packages/notifications/project.json | 16 + packages/notifications/src/index.ts | 0 packages/notifications/tsconfig.json | 12 + packages/notifications/tsconfig.lint.json | 8 + .../graphics/bridged-assets-v2-web-banner.png | Bin 0 -> 128449 bytes packages/ui/src/assets/index.ts | 1 + .../components/swipeablecards/BaseCard.tsx | 2 +- .../src/components/switch/Switch.native.tsx | 2 +- packages/ui/src/loading/Shine.native.tsx | 2 +- packages/ui/src/loading/Skeleton.native.tsx | 1 - .../ui/src/loading/SpinningLoader.native.tsx | 2 +- packages/uniswap/jest-package-mocks.js | 6 +- packages/uniswap/package.json | 10 +- .../BridgedAsset/BridgedAssetModal.tsx | 10 +- .../components/BridgedAsset/WormholeModal.tsx | 17 +- .../src/components/BridgedAsset/utils.ts | 23 - .../AmountInputPresets/AmountInputPresets.tsx | 37 +- .../AmountInputPresets/types.ts | 13 +- .../CurrencyInputPanel/CurrencyInputPanel.tsx | 31 +- .../CurrencyInputPanelHeader.tsx | 31 +- .../TokenSelector/TokenSelector.tsx | 3 +- .../TokenSelector/TokenSelectorList.tsx | 3 +- .../components/TokenSelector/hooks.test.ts | 31 +- .../details/TransactionDetailsModal.test.tsx | 3 +- .../SwapTransactionDetails.test.tsx | 3 +- .../TransferTransactionDetails.test.tsx | 3 +- .../gating/DynamicConfigDropdown.tsx | 4 +- .../src/components/gating/GatingOverrides.tsx | 23 +- .../uniswap/src/components/gating/Rows.tsx | 3 +- .../gating/dynamicConfigOverrides.tsx | 2 +- .../lists/items/pools/PoolOptionItem.tsx | 2 +- .../items/pools/PoolOptionItemContextMenu.tsx | 2 +- .../usePoolSearchResultsToPoolOptions.tsx | 2 +- .../items/pools/usePoolStatsToPoolOptions.tsx | 2 +- .../src/components/lists/items/types.ts | 2 +- .../uniswap/src/components/nfts/NftView.tsx | 6 +- .../uniswap/src/components/nfts/NftsList.tsx | 7 +- .../src/components/nfts/NftsList.web.tsx | 66 +- .../NotificationToast.native.tsx | 4 +- .../apiClients/tradingApi/TradingApiClient.ts | 3 +- .../apiClients/uniswapApi/useGasFeeQuery.ts | 2 +- .../conversionTracking/useConversionProxy.ts | 3 +- .../useConversionTracking.ts | 3 +- packages/uniswap/src/data/rest/getPair.ts | 19 - packages/uniswap/src/data/rest/getPools.ts | 4 +- .../uniswap/src/data/rest/getPoolsRewards.ts | 4 +- .../uniswap/src/data/rest/getPortfolio.ts | 5 +- packages/uniswap/src/data/rest/getPosition.ts | 4 +- .../uniswap/src/data/rest/getPositions.ts | 6 +- .../data/rest/portfolioBalanceOverrides.ts | 21 +- .../src/data/rest/searchTokensAndPools.ts | 61 +- .../activity/formatTransactionsByDate.ts | 21 +- .../src/features/behaviorHistory/slice.ts | 6 + .../src/features/chains/evm/info/avalanche.ts | 2 +- .../src/features/chains/evm/info/celo.ts | 2 +- .../src/features/chains/evm/info/mainnet.ts | 2 +- .../src/features/chains/evm/info/monad.ts | 2 +- .../src/features/chains/evm/info/polygon.ts | 2 +- .../src/features/chains/gasDefaults.ts | 2 +- .../chains/hooks/useFeatureFlaggedChainIds.ts | 3 +- .../features/chains/hooks/useNewChainIds.ts | 3 +- .../chains/hooks/useOrderedChainIds.ts | 3 +- packages/uniswap/src/features/chains/types.ts | 2 +- .../src/features/dataApi/balances/balances.ts | 31 +- .../features/dataApi/balances/balancesRest.ts | 3 +- .../tokenProjects/tokenProjects.test.tsx | 14 +- .../utils/tokenProjectToCurrencyInfos.test.ts | 2 + .../utils/tokenProjectToCurrencyInfos.ts | 5 +- .../uniswap/src/features/dataApi/types.ts | 5 + .../utils/gqlTokenToCurrencyInfo.test.ts | 2 + .../dataApi/utils/gqlTokenToCurrencyInfo.ts | 5 +- .../hooks/useFormatChartFiatDelta.ts | 31 + .../priceChart/formatters/shared/types.ts | 37 + .../priceChart/formatters/shared/utils.ts | 93 ++ .../formatters/stablecoinFormatter.ts | 84 ++ .../formatters/standardFormatter.ts | 88 ++ .../priceChart/priceChartConversion.test.ts | 1204 +++++++++++++++++ .../priceChart/priceChartConversion.ts | 42 + .../hooks/useForceUpgradeStatus.ts | 3 +- .../hooks/useForceUpgradeTranslations.ts | 8 +- packages/uniswap/src/features/gas/hooks.ts | 3 +- .../gas/hooks/useMaxAmountSpend.test.ts | 3 +- .../features/gas/hooks/useMaxAmountSpend.ts | 3 +- packages/uniswap/src/features/gas/utils.ts | 8 +- .../gating/StatsigProviderWrapper.tsx | 8 +- .../src/features/gating/statsigBaseConfig.ts | 3 +- .../uniswap/src/features/gating/typeGuards.ts | 2 +- .../uniswap/src/features/language/hooks.tsx | 2 +- .../nfts/hooks/useNftContextMenuItems.tsx | 3 +- .../passkey/hooks/useEmbeddedWalletBaseUrl.ts | 3 +- .../PortfolioBalance/PortfolioBalance.tsx | 35 +- .../uniswap/src/features/portfolio/api.ts | 3 +- .../isInstantTokenBalanceUpdateEnabled.ts | 3 +- .../rest/fetchOnChainBalancesRest.test.ts | 66 +- .../rest/fetchOnChainBalancesRest.ts | 87 +- ...estQueriesViaOnchainOverrideVariantSaga.ts | 16 +- .../src/features/providers/rpcUrlSelector.ts | 3 +- .../features/search/SearchHistoryResult.ts | 2 +- .../search/SearchModal/analytics/analytics.ts | 2 +- .../SearchModal/hooks/useFilterCallbacks.ts | 86 +- .../src/features/settings/hooks.test.ts | 3 +- .../smartWallet/mismatch/MismatchContext.tsx | 3 +- .../telemetry/constants/trace/element.ts | 2 + .../telemetry/constants/trace/section.ts | 1 + .../features/telemetry/constants/uniswap.ts | 7 +- .../uniswap/src/features/telemetry/types.ts | 4 + .../utils/logExperimentQualifyingEvent.ts | 9 + .../useBlockaidFeeComparisonAnalytics.ts | 3 +- .../DecimalPadInput/DecimalPad.native.tsx | 2 +- .../TransactionSettingsRow.tsx | 3 +- .../slippage/useSlippageSettings.ts | 2 +- .../transactions/components/settings/types.ts | 2 +- .../hooks/useGetCanSignPermits.ts | 3 +- .../hooks/useGetSwapDelegationAddress.ts | 3 +- .../hooks/usePollingIntervalByChain.ts | 4 +- .../features/transactions/liquidity/types.ts | 2 +- .../src/features/transactions/steps/types.ts | 49 + .../SlippageInfo/SlippageInfoCaption.tsx | 2 +- .../hooks/useSwapFormButtonText.ts | 3 +- .../TradeRoutingPreferenceScreen.tsx | 4 +- .../AnimatedTokenFlip.tsx | 2 +- .../GradientContainer.native.tsx | 2 +- .../GradientContainer.web.tsx | 2 +- .../useReceiptSuccessHandler.ts | 24 +- .../hooks/receiptFetching/utils.ts | 78 ++ .../SwapFormDecimalPad.native.tsx | 31 +- .../swap/hooks/useIsForFiltersEnabled.ts | 3 +- .../hooks/useIsUnichainFlashblocksEnabled.ts | 129 +- .../swap/hooks/useNeedsBridgedAssetWarning.ts | 10 +- .../swap/hooks/usePriceUXEnabled.ts | 3 +- .../transactions/swap/plan/planSaga.ts | 243 ++++ .../swap/plan/planStepTransformer.ts | 62 + .../features/transactions/swap/plan/utils.ts | 35 + .../hooks/useCreateSwapReviewCallbacks.tsx | 3 +- .../services/swapTxAndGasInfoService/hooks.ts | 3 +- .../swapFormStore/hooks/useDerivedSwapInfo.ts | 3 +- .../SwapTxStoreContextProvider.tsx | 3 +- .../hooks/useTransactionRequestInfo.test.ts | 4 +- .../hooks/useTransactionRequestInfo.ts | 4 +- .../features/transactions/swap/types/trade.ts | 1 + .../swap/utils/getIsWebForNudgeEnabled.ts | 3 +- .../transactions/swap/utils/protocols.test.ts | 6 +- .../transactions/swap/utils/protocols.ts | 4 +- .../transactions/swap/utils/routing.ts | 12 +- .../features/transactions/swap/utils/trade.ts | 10 + .../swap/utils/tradingApi.test.ts | 5 +- .../transactions/swap/utils/tradingApi.ts | 3 +- .../features/unitags/ClaimUnitagContent.tsx | 18 +- .../src/i18n/locales/source/en-US.json | 26 +- .../src/i18n/locales/translations/af-ZA.json | 3 +- .../src/i18n/locales/translations/ar-SA.json | 3 +- .../src/i18n/locales/translations/ca-ES.json | 3 +- .../src/i18n/locales/translations/da-DK.json | 3 +- .../src/i18n/locales/translations/el-GR.json | 3 +- .../src/i18n/locales/translations/es-ES.json | 28 +- .../src/i18n/locales/translations/fi-FI.json | 3 +- .../src/i18n/locales/translations/fil-PH.json | 28 +- .../src/i18n/locales/translations/fr-FR.json | 28 +- .../src/i18n/locales/translations/he-IL.json | 3 +- .../src/i18n/locales/translations/hi-IN.json | 3 +- .../src/i18n/locales/translations/hu-HU.json | 3 +- .../src/i18n/locales/translations/id-ID.json | 28 +- .../src/i18n/locales/translations/it-IT.json | 3 +- .../src/i18n/locales/translations/ja-JP.json | 28 +- .../src/i18n/locales/translations/ko-KR.json | 28 +- .../src/i18n/locales/translations/ms-MY.json | 3 +- .../src/i18n/locales/translations/nl-NL.json | 28 +- .../src/i18n/locales/translations/pl-PL.json | 3 +- .../src/i18n/locales/translations/pt-PT.json | 28 +- .../src/i18n/locales/translations/ru-RU.json | 28 +- .../src/i18n/locales/translations/sl-SI.json | 3 +- .../src/i18n/locales/translations/sr-SP.json | 3 +- .../src/i18n/locales/translations/sv-SE.json | 3 +- .../src/i18n/locales/translations/sw-TZ.json | 3 +- .../src/i18n/locales/translations/tr-TR.json | 28 +- .../src/i18n/locales/translations/uk-UA.json | 3 +- .../src/i18n/locales/translations/ur-PK.json | 3 +- .../src/i18n/locales/translations/vi-VN.json | 28 +- .../src/i18n/locales/translations/zh-CN.json | 28 +- .../src/i18n/locales/translations/zh-TW.json | 28 +- packages/uniswap/src/state/oldTypes.ts | 2 +- .../src/test/fixtures/gql/assets/tokens.ts | 2 + packages/uniswap/src/test/fixtures/testIDs.ts | 2 + packages/uniswap/src/test/mocks/gql/mocks.ts | 2 + packages/uniswap/src/utils/datadog.web.ts | 17 +- .../search/doesTokenMatchSearchTerm.test.ts | 419 ++++++ .../utils/search/doesTokenMatchSearchTerm.ts | 37 + ...etPossibleChainMatchFromSearchWord.test.ts | 437 ++++++ .../getPossibleChainMatchFromSearchWord.ts | 47 + .../parseChainFromTokenSearchQuery.test.ts | 138 ++ .../search/parseChainFromTokenSearchQuery.ts | 81 ++ packages/uniswap/tsconfig.json | 3 + packages/wallet/package.json | 3 +- .../components/landing/LandingBackground.tsx | 4 +- .../smartWallet/smartAccounts/hooks.ts | 3 +- .../src/features/gating/userPropertyHooks.ts | 2 +- .../smartWallet/hooks/useNetworkBalances.tsx | 2 +- .../contexts/WalletUniswapContext.tsx | 3 +- .../services/featureFlagService.ts | 4 +- .../services/featureFlagServiceImpl.ts | 4 +- .../transactionConfigServiceImpl.test.ts | 2 +- .../services/transactionConfigServiceImpl.ts | 3 +- .../executeTransaction/tryGetNonce.ts | 12 +- .../transactions/send/TokenSelectorPanel.tsx | 1 - .../transactions/swap/confirmation.ts | 3 +- .../transactions/swap/executeSwapSaga.ts | 3 +- .../swap/hooks/useSwapHandlers.ts | 3 +- .../swap/modals/QueuedOrderModal.tsx | 3 +- .../swap/prepareAndSignSwapSaga.test.ts | 3 +- .../swap/prepareAndSignSwapSaga.ts | 3 +- .../swap/settings/SwapProtection.tsx | 2 +- .../transactions/swap/swapSaga.test.ts | 3 +- .../features/transactions/swap/swapSaga.ts | 22 +- .../watcher/transactionFinalizationSaga.ts | 3 +- .../watchOnChainTransactionSaga.test.ts | 3 +- .../watcher/watchOnChainTransactionSaga.ts | 3 +- packages/wallet/tsconfig.json | 3 + scripts/clean.sh | 88 +- scripts/remove-local-packages.sh | 14 + .../src/generators/package/files/biome.json | 6 - .../generators/package/files/tsconfig.json | 2 +- .../src/generators/package/package.ts | 28 +- tsconfig.base.json | 4 +- tsconfig.json | 6 + 598 files changed, 11278 insertions(+), 2162 deletions(-) create mode 100644 apps/mobile/src/components/PriceExplorer/useFiatDelta.tsx create mode 100644 apps/web/public/images/portfolio_page_promo/dark.png create mode 100644 apps/web/public/images/portfolio_page_promo/light.png create mode 100644 apps/web/src/assets/images/portfolio-page-promo/dark.svg create mode 100644 apps/web/src/assets/images/portfolio-page-promo/light.svg create mode 100644 apps/web/src/assets/svg/Emblem/A.svg create mode 100644 apps/web/src/assets/svg/Emblem/B.svg create mode 100644 apps/web/src/assets/svg/Emblem/C.svg create mode 100644 apps/web/src/assets/svg/Emblem/D.svg create mode 100644 apps/web/src/assets/svg/Emblem/E.svg create mode 100644 apps/web/src/assets/svg/Emblem/F.svg create mode 100644 apps/web/src/assets/svg/Emblem/G.svg create mode 100644 apps/web/src/assets/svg/Emblem/default.svg create mode 100644 apps/web/src/components/ActivityTable/ActivityAddressCell.tsx create mode 100644 apps/web/src/components/ActivityTable/ActivityAmountCell.tsx create mode 100644 apps/web/src/components/ActivityTable/ActivityTable.tsx create mode 100644 apps/web/src/components/ActivityTable/AddressWithAvatar.tsx create mode 100644 apps/web/src/components/ActivityTable/TimeCell.tsx create mode 100644 apps/web/src/components/ActivityTable/TokenAmountDisplay.tsx create mode 100644 apps/web/src/components/ActivityTable/TransactionTypeCell.tsx create mode 100644 apps/web/src/components/ActivityTable/activityTableModels.ts create mode 100644 apps/web/src/components/ActivityTable/registry.ts create mode 100644 apps/web/src/components/Banner/BridgingPopularTokens/BridgingPopularTokensBanner.tsx create mode 100644 apps/web/src/components/Web3Provider/rejectableConnector.ts create mode 100644 apps/web/src/pages/Portfolio/ConnectWalletBanner.tsx create mode 100644 apps/web/src/pages/Portfolio/ConnectWalletBottomOverlay.tsx delete mode 100644 apps/web/src/pages/Portfolio/ConnectWalletView.tsx create mode 100644 apps/web/src/pages/Portfolio/Header/PortfolioAddressDisplay/ConnectedAddressDisplay.tsx create mode 100644 apps/web/src/pages/Portfolio/Header/PortfolioAddressDisplay/DemoAddressDisplay.tsx create mode 100644 apps/web/src/pages/Portfolio/Header/PortfolioAddressDisplay/PortfolioAddressDisplay.tsx create mode 100644 apps/web/src/pages/Portfolio/Header/hooks/useIsConnected.ts create mode 100644 apps/web/src/pages/Portfolio/NFTs/NFTCard.tsx create mode 100644 apps/web/src/pages/Portfolio/NFTs/Nfts.tsx create mode 100644 apps/web/src/pages/Portfolio/NFTs/utils/filterNfts.test.ts create mode 100644 apps/web/src/pages/Portfolio/NFTs/utils/filterNfts.ts delete mode 100644 apps/web/src/pages/Portfolio/Nfts.tsx create mode 100644 apps/web/src/pages/Portfolio/PortfolioContent.tsx create mode 100644 apps/web/src/pages/Portfolio/PortfolioDisconnectedView.tsx create mode 100644 apps/web/src/pages/Portfolio/Tokens/Table/TokensTable.tsx rename apps/web/src/pages/Portfolio/Tokens/Table/{Table.tsx => TokensTableInner.tsx} (66%) create mode 100644 apps/web/src/pages/Portfolio/Tokens/utils/filterTokensBySearch.test.ts create mode 100644 apps/web/src/pages/Portfolio/Tokens/utils/filterTokensBySearch.ts create mode 100644 apps/web/src/pages/Portfolio/hooks/usePortfolioAddress.ts create mode 100644 packages/api/src/clients/notifications/createNotificationsApiClient.ts create mode 100644 packages/api/src/clients/notifications/types.ts create mode 100644 packages/gating/.eslintrc.js create mode 100644 packages/gating/README.md create mode 100644 packages/gating/package.json create mode 100644 packages/gating/project.json rename packages/{uniswap/src/features/gating => gating/src}/LocalOverrideAdapterWrapper.ts (95%) rename packages/{uniswap/src/features/gating => gating/src}/configs.ts (81%) rename packages/{uniswap/src/features/gating => gating/src}/constants.ts (100%) rename packages/{uniswap/src/features/gating => gating/src}/experiments.ts (83%) rename packages/{uniswap/src/features/gating => gating/src}/flags.ts (98%) rename packages/{uniswap/src/features/gating => gating/src}/getStatsigEnvName.ts (100%) rename packages/{uniswap/src/features/gating => gating/src}/hooks.ts (95%) create mode 100644 packages/gating/src/index.ts rename packages/{uniswap/src/features/gating => gating/src}/sdk/statsig.native.ts (83%) rename packages/{uniswap/src/features/gating => gating/src}/sdk/statsig.ts (92%) rename packages/{uniswap/src/features/gating => gating/src}/utils.ts (92%) create mode 100644 packages/gating/tsconfig.json create mode 100644 packages/gating/tsconfig.lint.json create mode 100644 packages/notifications/.eslintrc.js create mode 100644 packages/notifications/README.md create mode 100644 packages/notifications/package.json create mode 100644 packages/notifications/project.json create mode 100644 packages/notifications/src/index.ts create mode 100644 packages/notifications/tsconfig.json create mode 100644 packages/notifications/tsconfig.lint.json create mode 100644 packages/ui/src/assets/graphics/bridged-assets-v2-web-banner.png delete mode 100644 packages/uniswap/src/components/BridgedAsset/utils.ts delete mode 100644 packages/uniswap/src/data/rest/getPair.ts create mode 100644 packages/uniswap/src/features/fiatCurrency/hooks/useFormatChartFiatDelta.ts create mode 100644 packages/uniswap/src/features/fiatCurrency/priceChart/formatters/shared/types.ts create mode 100644 packages/uniswap/src/features/fiatCurrency/priceChart/formatters/shared/utils.ts create mode 100644 packages/uniswap/src/features/fiatCurrency/priceChart/formatters/stablecoinFormatter.ts create mode 100644 packages/uniswap/src/features/fiatCurrency/priceChart/formatters/standardFormatter.ts create mode 100644 packages/uniswap/src/features/fiatCurrency/priceChart/priceChartConversion.test.ts create mode 100644 packages/uniswap/src/features/fiatCurrency/priceChart/priceChartConversion.ts create mode 100644 packages/uniswap/src/features/telemetry/utils/logExperimentQualifyingEvent.ts create mode 100644 packages/uniswap/src/features/transactions/swap/plan/planSaga.ts create mode 100644 packages/uniswap/src/features/transactions/swap/plan/planStepTransformer.ts create mode 100644 packages/uniswap/src/features/transactions/swap/plan/utils.ts create mode 100644 packages/uniswap/src/utils/search/doesTokenMatchSearchTerm.test.ts create mode 100644 packages/uniswap/src/utils/search/doesTokenMatchSearchTerm.ts create mode 100644 packages/uniswap/src/utils/search/getPossibleChainMatchFromSearchWord.test.ts create mode 100644 packages/uniswap/src/utils/search/getPossibleChainMatchFromSearchWord.ts create mode 100644 packages/uniswap/src/utils/search/parseChainFromTokenSearchQuery.test.ts create mode 100644 packages/uniswap/src/utils/search/parseChainFromTokenSearchQuery.ts create mode 100755 scripts/remove-local-packages.sh delete mode 100644 tools/uniswap-nx/src/generators/package/files/biome.json diff --git a/.gitignore b/.gitignore index 3b3a172871a..b3c573b208b 100644 --- a/.gitignore +++ b/.gitignore @@ -76,9 +76,10 @@ CLAUDE.local.md # lefthook .lefthook/ - - +# Nx .nx/cache .nx/workspace-data -.cursor/rules/nx-rules.mdc .github/instructions/nx.instructions.md + +# Spec Workflow MCP +.spec-workflow/ diff --git a/RELEASE b/RELEASE index 80f8e3ca3e2..12680356c4b 100644 --- a/RELEASE +++ b/RELEASE @@ -1,6 +1,6 @@ IPFS hash of the deployment: -- CIDv0: `QmZmRjJXcRL1HVbuFBLX23qqmQydzmqGsLmb94qrqTU9A7` -- CIDv1: `bafybeifjzfti2rq27be42zfwqnturszxqyecs4zxklxxr4pejgcgi72eja` +- CIDv0: `QmUF5sYAgjigJ6D7nE6p12SG1rFrwYLidKxeX52qBN1Pce` +- CIDv1: `bafybeicxxeljdmh6f2s3xo43piks2h7ibgm6hbfoo3ftc5wjfmqfsngghm` The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org). @@ -10,14 +10,54 @@ You can also access the Uniswap Interface from an IPFS gateway. Your Uniswap settings are never remembered across different URLs. IPFS gateways: -- https://bafybeifjzfti2rq27be42zfwqnturszxqyecs4zxklxxr4pejgcgi72eja.ipfs.dweb.link/ -- [ipfs://QmZmRjJXcRL1HVbuFBLX23qqmQydzmqGsLmb94qrqTU9A7/](ipfs://QmZmRjJXcRL1HVbuFBLX23qqmQydzmqGsLmb94qrqTU9A7/) +- https://bafybeicxxeljdmh6f2s3xo43piks2h7ibgm6hbfoo3ftc5wjfmqfsngghm.ipfs.dweb.link/ +- [ipfs://QmUF5sYAgjigJ6D7nE6p12SG1rFrwYLidKxeX52qBN1Pce/](ipfs://QmUF5sYAgjigJ6D7nE6p12SG1rFrwYLidKxeX52qBN1Pce/) -## 5.115.0 (2025-10-24) +## 5.116.0 (2025-10-28) ### Features -* **web:** special case metamask dual vm connection flow (#24756) (#24789) b21eafd +* **web:** add activity table to the tab with real data (#23506) f00228c +* **web:** Add createRejectableMockConnector util to force tx rejection (#24574) 3b3b2b7 +* **web:** add demo account support for activity tab (#24639) 9ec0194 +* **web:** add disconnected portfolio view (#23690) 7a1b085 +* **web:** add fiat to price chart (#23577) fab99ce +* **web:** add hidden tokens table rows (#23535) 291fab3 +* **web:** add loading state to tokens table (#23544) ed5ced8 +* **web:** add more & better filtering + transaction parsing (#24579) 205c03d +* **web:** add v2 bridged asset banner (#24734) 4666868 +* **web:** disconnected view B version (#24630) 46ca828 +* **web:** Help Modal styling nits (#24547) ae252e6 +* **web:** NFTs tab (#23604) a438b54 +* **web:** small style nits for Company menu (#24318) 4d71e08 +* **web:** special case metamask dual vm connection flow (#24756) faabc72 +* **web:** tokens table search (#23509) b83fc75 +* **web:** update CompanyMenu arrangement on tablet width (#24312) 758f68d + + +### Bug Fixes + +* **web:** default to mainnet for limits flow [STAGING] (#24885) 5a8e150 +* **web:** Fix CreatePosition e2e anvil test (#24573) d68b011 +* **web:** Fix e2e anvil tests missing quote stub (#24590) 838d5bd +* **web:** Fix limit order chain switch bug (#23064) b11176d +* **web:** Fix Swap e2e anvil tests (#24662) 26adf5c +* **web:** fixes pools tab loader skeletons (#24472) 2f887aa +* **web:** Increase anvil manager timeout (#24623) 466eb69 +* **web:** log interface swap finalization results for flashblocks (#24869) bf30270 +* **web:** support chain filtering query params (#24754) 4bc3729 +* **web:** update the create flow to display the latest dependnet amount (#24676) 168c20a +* **web:** Use Mainnet instead of Base for e2e test commands (#24589) ff7dfee + + +### Continuous Integration + +* **web:** update sitemaps 4e8124b + + +### Tests + +* **web:** Disable anvil snapshots by default (#24666) 1a2903c diff --git a/VERSION b/VERSION index 885f8abd813..a4fafce7ea0 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -web/5.115.0 \ No newline at end of file +web/5.116.0 \ No newline at end of file diff --git a/apps/extension/package.json b/apps/extension/package.json index 3409945e3e4..02974e473d0 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -22,6 +22,7 @@ "@uniswap/v3-sdk": "3.25.2", "@uniswap/v4-sdk": "1.21.2", "@universe/api": "workspace:^", + "@universe/gating": "workspace:^", "@wxt-dev/module-react": "1.1.3", "confusing-browser-globals": "1.0.11", "dotenv-webpack": "8.0.1", diff --git a/apps/extension/src/app/apollo.tsx b/apps/extension/src/app/apollo.tsx index 41416c70604..e006c3ed06a 100644 --- a/apps/extension/src/app/apollo.tsx +++ b/apps/extension/src/app/apollo.tsx @@ -1,8 +1,7 @@ import { ApolloProvider } from '@apollo/client/react/context' -import { PropsWithChildren, useEffect } from 'react' +import { PropsWithChildren } from 'react' import { localStorage } from 'redux-persist-webextension-storage' import { getReduxStore } from 'src/store/store' -import { initializePortfolioQueryOverrides } from 'uniswap/src/data/rest/portfolioBalanceOverrides' // biome-ignore lint/style/noRestrictedImports: Direct wallet import needed for Apollo client setup in extension context import { usePersistedApolloClient } from 'wallet/src/data/apollo/usePersistedApolloClient' @@ -16,14 +15,9 @@ export function GraphqlProvider({ children }: PropsWithChildren): JSX.E reduxStore: getReduxStore(), }) - useEffect(() => { - if (apolloClient) { - initializePortfolioQueryOverrides({ store: getReduxStore(), apolloClient }) - } - }, [apolloClient]) - if (!apolloClient) { return <> } + return {children} } diff --git a/apps/extension/src/app/components/AutoLockProvider.tsx b/apps/extension/src/app/components/AutoLockProvider.tsx index 4d7989e0fc7..8f26d940352 100644 --- a/apps/extension/src/app/components/AutoLockProvider.tsx +++ b/apps/extension/src/app/components/AutoLockProvider.tsx @@ -1,9 +1,8 @@ +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { PropsWithChildren, useEffect } from 'react' import { useSelector } from 'react-redux' import { ExtensionState } from 'src/store/extensionReducer' import { useIsChromeWindowFocusedWithTimeout } from 'uniswap/src/extension/useIsChromeWindowFocused' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { deviceAccessTimeoutToMs } from 'uniswap/src/features/settings/constants' import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' diff --git a/apps/extension/src/app/core/StatsigProvider.tsx b/apps/extension/src/app/core/StatsigProvider.tsx index 63f55e72d4f..7c6aeb01b76 100644 --- a/apps/extension/src/app/core/StatsigProvider.tsx +++ b/apps/extension/src/app/core/StatsigProvider.tsx @@ -1,10 +1,9 @@ import { useQuery } from '@tanstack/react-query' import { SharedQueryClient } from '@universe/api' +import { StatsigCustomAppValue, StatsigUser } from '@universe/gating' import { useEffect, useState } from 'react' import { makeStatsigUser } from 'src/app/core/initStatSigForBrowserScripts' -import { StatsigCustomAppValue } from 'uniswap/src/features/gating/constants' import { StatsigProviderWrapper } from 'uniswap/src/features/gating/StatsigProviderWrapper' -import { StatsigUser } from 'uniswap/src/features/gating/sdk/statsig' import { initializeDatadog } from 'uniswap/src/utils/datadog' import { uniqueIdQuery } from 'utilities/src/device/uniqueIdQuery' diff --git a/apps/extension/src/app/core/initStatSigForBrowserScripts.tsx b/apps/extension/src/app/core/initStatSigForBrowserScripts.tsx index 3e3ffaaa9ba..7a495477c72 100644 --- a/apps/extension/src/app/core/initStatSigForBrowserScripts.tsx +++ b/apps/extension/src/app/core/initStatSigForBrowserScripts.tsx @@ -1,6 +1,5 @@ +import { StatsigClient, StatsigCustomAppValue, StatsigUser } from '@universe/gating' import { config } from 'uniswap/src/config' -import { StatsigCustomAppValue } from 'uniswap/src/features/gating/constants' -import { StatsigClient, StatsigUser } from 'uniswap/src/features/gating/sdk/statsig' import { statsigBaseConfig } from 'uniswap/src/features/gating/statsigBaseConfig' import { getUniqueId } from 'utilities/src/device/uniqueId' import { logger } from 'utilities/src/logger/logger' diff --git a/apps/extension/src/app/features/biometricUnlock/useShouldShowBiometricUnlock.ts b/apps/extension/src/app/features/biometricUnlock/useShouldShowBiometricUnlock.ts index 3491a94d6bb..54582813431 100644 --- a/apps/extension/src/app/features/biometricUnlock/useShouldShowBiometricUnlock.ts +++ b/apps/extension/src/app/features/biometricUnlock/useShouldShowBiometricUnlock.ts @@ -1,7 +1,6 @@ import { useQuery } from '@tanstack/react-query' +import { DynamicConfigs, ExtensionBiometricUnlockConfigKey, useDynamicConfigValue } from '@universe/gating' import { biometricUnlockCredentialQuery } from 'src/app/features/biometricUnlock/biometricUnlockCredentialQuery' -import { DynamicConfigs, ExtensionBiometricUnlockConfigKey } from 'uniswap/src/features/gating/configs' -import { useDynamicConfigValue } from 'uniswap/src/features/gating/hooks' export function useShouldShowBiometricUnlock(): boolean { const isEnabled = useDynamicConfigValue({ diff --git a/apps/extension/src/app/features/biometricUnlock/useShouldShowBiometricUnlockEnrollment.ts b/apps/extension/src/app/features/biometricUnlock/useShouldShowBiometricUnlockEnrollment.ts index 7c496296c1d..f642af2d9ad 100644 --- a/apps/extension/src/app/features/biometricUnlock/useShouldShowBiometricUnlockEnrollment.ts +++ b/apps/extension/src/app/features/biometricUnlock/useShouldShowBiometricUnlockEnrollment.ts @@ -1,8 +1,7 @@ import { useQuery } from '@tanstack/react-query' +import { DynamicConfigs, ExtensionBiometricUnlockConfigKey, useDynamicConfigValue } from '@universe/gating' import { useTranslation } from 'react-i18next' import { builtInBiometricCapabilitiesQuery } from 'src/app/utils/device/builtInBiometricCapabilitiesQuery' -import { DynamicConfigs, ExtensionBiometricUnlockConfigKey } from 'uniswap/src/features/gating/configs' -import { useDynamicConfigValue } from 'uniswap/src/features/gating/hooks' export function useShouldShowBiometricUnlockEnrollment({ flow }: { flow: 'onboarding' | 'settings' }): boolean { const { t } = useTranslation() diff --git a/apps/extension/src/app/features/dappRequests/requestContent/SignTypeData/SignTypedDataRequestContent.tsx b/apps/extension/src/app/features/dappRequests/requestContent/SignTypeData/SignTypedDataRequestContent.tsx index 19606509983..b015627b61b 100644 --- a/apps/extension/src/app/features/dappRequests/requestContent/SignTypeData/SignTypedDataRequestContent.tsx +++ b/apps/extension/src/app/features/dappRequests/requestContent/SignTypeData/SignTypedDataRequestContent.tsx @@ -1,3 +1,4 @@ +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { useTranslation } from 'react-i18next' import { DappRequestContent } from 'src/app/features/dappRequests/DappRequestContent' import { ActionCanNotBeCompletedContent } from 'src/app/features/dappRequests/requestContent/ActionCanNotBeCompleted/ActionCanNotBeCompletedContent' @@ -11,8 +12,6 @@ import { EIP712Message, isEIP712TypedData } from 'src/app/features/dappRequests/ import { isPermit2, isUniswapXSwapRequest } from 'src/app/features/dappRequests/types/Permit2Types' import { Flex, Text } from 'ui/src' import { toSupportedChainId } from 'uniswap/src/features/chains/utils' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { useHasAccountMismatchCallback } from 'uniswap/src/features/smartWallet/mismatch/hooks' import { ExplorerDataType, getExplorerLink } from 'uniswap/src/utils/linking' import { isEVMAddressWithChecksum } from 'utilities/src/addresses/evm/evm' diff --git a/apps/extension/src/app/features/dappRequests/saga.ts b/apps/extension/src/app/features/dappRequests/saga.ts index 8ce3f50de43..7a511bca103 100644 --- a/apps/extension/src/app/features/dappRequests/saga.ts +++ b/apps/extension/src/app/features/dappRequests/saga.ts @@ -1,6 +1,7 @@ /* eslint-disable max-lines */ import { Provider } from '@ethersproject/providers' import { providerErrors, rpcErrors, serializeError } from '@metamask/rpc-errors' +import { FeatureFlags, getFeatureFlag } from '@universe/gating' import { createSearchParams } from 'react-router' import { changeChain } from 'src/app/features/dapp/changeChain' import { DappInfo, dappStore } from 'src/app/features/dapp/store' @@ -45,8 +46,6 @@ import getCalldataInfoFromTransaction from 'src/background/utils/getCalldataInfo import { call, put, select, take } from 'typed-redux-saga' import { hexadecimalStringToInt, toSupportedChainId } from 'uniswap/src/features/chains/utils' import { DappRequestType, DappResponseType } from 'uniswap/src/features/dappRequests/types' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { getFeatureFlag } from 'uniswap/src/features/gating/hooks' import { pushNotification } from 'uniswap/src/features/notifications/slice/slice' import { AppNotificationType } from 'uniswap/src/features/notifications/slice/types' import { Platform } from 'uniswap/src/features/platforms/types/Platform' diff --git a/apps/extension/src/app/features/home/HomeScreen.tsx b/apps/extension/src/app/features/home/HomeScreen.tsx index f7a5e10a591..00145f31da7 100644 --- a/apps/extension/src/app/features/home/HomeScreen.tsx +++ b/apps/extension/src/app/features/home/HomeScreen.tsx @@ -1,5 +1,6 @@ import { useApolloClient } from '@apollo/client' import { SharedEventName } from '@uniswap/analytics-events' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { memo, useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' @@ -21,8 +22,6 @@ import { navigate } from 'src/app/navigation/state' import { Flex, Loader, styled, Text, TouchableArea } from 'ui/src' import { SMART_WALLET_UPGRADE_VIDEO } from 'ui/src/assets' import { NFTS_TAB_DATA_DEPENDENCIES } from 'uniswap/src/components/nfts/constants' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { useSelectAddressHasNotifications } from 'uniswap/src/features/notifications/slice/hooks' import { setNotificationStatus } from 'uniswap/src/features/notifications/slice/slice' import { PortfolioBalance } from 'uniswap/src/features/portfolio/PortfolioBalance/PortfolioBalance' diff --git a/apps/extension/src/app/features/home/PortfolioActionButtons.tsx b/apps/extension/src/app/features/home/PortfolioActionButtons.tsx index a0ac5b56037..9a7a08625ce 100644 --- a/apps/extension/src/app/features/home/PortfolioActionButtons.tsx +++ b/apps/extension/src/app/features/home/PortfolioActionButtons.tsx @@ -1,4 +1,5 @@ import { SharedEventName } from '@uniswap/analytics-events' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { cloneElement, memo, useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { useInterfaceBuyNavigator } from 'src/app/features/for/utils' @@ -7,8 +8,6 @@ import { navigate } from 'src/app/navigation/state' import { Flex, getTokenValue, Text, TouchableArea, useMedia } from 'ui/src' import { ArrowDownCircle, Bank, CoinConvert, SendAction } from 'ui/src/components/icons' import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { ElementName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { TestnetModeModal } from 'uniswap/src/features/testnets/TestnetModeModal' diff --git a/apps/extension/src/app/features/home/PortfolioHeader.tsx b/apps/extension/src/app/features/home/PortfolioHeader.tsx index 45df41829bc..b951fd5c50a 100644 --- a/apps/extension/src/app/features/home/PortfolioHeader.tsx +++ b/apps/extension/src/app/features/home/PortfolioHeader.tsx @@ -55,7 +55,7 @@ const RotatingSettingsIcon = ({ onPressSettings }: { onPressSettings(): void }): }), ) } - }, [isScreenFocused, pressProgress]) + }, [isScreenFocused]) const onBegin = (): void => { pressProgress.value = withTiming(1) diff --git a/apps/extension/src/app/features/onboarding/import/SelectWallets.tsx b/apps/extension/src/app/features/onboarding/import/SelectWallets.tsx index dcf3940bb5e..3c8597824a0 100644 --- a/apps/extension/src/app/features/onboarding/import/SelectWallets.tsx +++ b/apps/extension/src/app/features/onboarding/import/SelectWallets.tsx @@ -1,4 +1,5 @@ import { useQuery } from '@tanstack/react-query' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { ComponentProps, useMemo, useState } from 'react' import { Trans, useTranslation } from 'react-i18next' import { SelectWalletsSkeleton } from 'src/app/components/loading/SelectWalletSkeleton' @@ -9,8 +10,6 @@ import { Flex, ScrollView, SpinningLoader, Square, Text, Tooltip, TouchableArea import { WalletFilled } from 'ui/src/components/icons' import { iconSizes } from 'ui/src/theme' import { uniswapUrls } from 'uniswap/src/constants/urls' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import Trace from 'uniswap/src/features/telemetry/Trace' import { ExtensionOnboardingFlow, ExtensionOnboardingScreens } from 'uniswap/src/types/screens/extension' import { openUri } from 'uniswap/src/utils/linking' diff --git a/apps/extension/src/app/features/onboarding/scan/ScanToOnboard.tsx b/apps/extension/src/app/features/onboarding/scan/ScanToOnboard.tsx index 30080a5b13f..3e01afb4a99 100644 --- a/apps/extension/src/app/features/onboarding/scan/ScanToOnboard.tsx +++ b/apps/extension/src/app/features/onboarding/scan/ScanToOnboard.tsx @@ -174,7 +174,7 @@ export function ScanToOnboard(): JSX.Element { ) return () => cancelAnimation(qrScale) - }, [isLoadingUUID, qrScale]) + }, [isLoadingUUID]) // Using useAnimatedStyle and AnimatedFlex because tamagui scale animation not working const qrAnimatedStyle = useAnimatedStyle(() => { return { diff --git a/apps/extension/src/app/features/settings/SettingsScreen.tsx b/apps/extension/src/app/features/settings/SettingsScreen.tsx index 4dd4947e716..ed9a068a3c1 100644 --- a/apps/extension/src/app/features/settings/SettingsScreen.tsx +++ b/apps/extension/src/app/features/settings/SettingsScreen.tsx @@ -1,3 +1,4 @@ +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' @@ -31,8 +32,6 @@ import { resetUniswapBehaviorHistory } from 'uniswap/src/features/behaviorHistor import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { FiatCurrency, ORDERED_CURRENCIES } from 'uniswap/src/features/fiatCurrency/constants' import { getFiatCurrencyName, useAppFiatCurrencyInfo } from 'uniswap/src/features/fiatCurrency/hooks' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { useCurrentLanguageInfo } from 'uniswap/src/features/language/hooks' import { PasskeyManagementModal } from 'uniswap/src/features/passkey/PasskeyManagementModal' import { setCurrentFiatCurrency, setIsTestnetModeEnabled } from 'uniswap/src/features/settings/slice' diff --git a/apps/extension/src/app/hooks/useIsExtensionPasskeyImportEnabled.ts b/apps/extension/src/app/hooks/useIsExtensionPasskeyImportEnabled.ts index 8abf941051b..823ac6ba868 100644 --- a/apps/extension/src/app/hooks/useIsExtensionPasskeyImportEnabled.ts +++ b/apps/extension/src/app/hooks/useIsExtensionPasskeyImportEnabled.ts @@ -1,5 +1,4 @@ -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' export function useIsExtensionPasskeyImportEnabled(): boolean { return useFeatureFlag(FeatureFlags.EmbeddedWallet) diff --git a/apps/extension/src/entrypoints/sidepanel/main.tsx b/apps/extension/src/entrypoints/sidepanel/main.tsx index 4cbeaaa35d0..5465a71e672 100644 --- a/apps/extension/src/entrypoints/sidepanel/main.tsx +++ b/apps/extension/src/entrypoints/sidepanel/main.tsx @@ -11,8 +11,10 @@ import { createRoot } from 'react-dom/client' import SidebarApp from 'src/app/core/SidebarApp' import { onboardingMessageChannel } from 'src/background/messagePassing/messageChannels' import { OnboardingMessageType } from 'src/background/messagePassing/types/ExtensionMessages' +import { getReduxStore } from 'src/store/store' import { ExtensionAppLocation, StoreSynchronization } from 'src/store/storeSynchronization' import { initializeScrollWatcher } from 'uniswap/src/components/modals/ScrollLock' +import { initializePortfolioQueryOverrides } from 'uniswap/src/data/rest/portfolioBalanceOverrides' import { logger } from 'utilities/src/logger/logger' // biome-ignore lint/suspicious/noExplicitAny: Global polyfill cleanup requires any type for runtime modification ;(globalThis as any).regeneratorRuntime = undefined @@ -44,6 +46,7 @@ export function makeSidebar(): void { } StoreSynchronization.init(ExtensionAppLocation.SidePanel) + initializePortfolioQueryOverrides({ store: getReduxStore() }) initSidebar() initializeScrollWatcher() } diff --git a/apps/extension/tsconfig.json b/apps/extension/tsconfig.json index 007f0494490..831ce04a0d1 100644 --- a/apps/extension/tsconfig.json +++ b/apps/extension/tsconfig.json @@ -19,6 +19,9 @@ }, { "path": "../../packages/api" + }, + { + "path": "../../packages/gating" } ], "compilerOptions": { diff --git a/apps/mobile/.maestro/flows/deeplinks/deeplink-comprehensive.yaml b/apps/mobile/.maestro/flows/deeplinks/deeplink-comprehensive.yaml index 7986a843f72..71df2ffe3af 100644 --- a/apps/mobile/.maestro/flows/deeplinks/deeplink-comprehensive.yaml +++ b/apps/mobile/.maestro/flows/deeplinks/deeplink-comprehensive.yaml @@ -12,7 +12,7 @@ env: - runScript: file: ../../scripts/performance/dist/actions/start-flow.js env: - FLOW_NAME: 'deeplink-comprehensive' + FLOW_NAME: "deeplink-comprehensive" # Run prerequisite flows (tracked as sub-flows) - runFlow: ../../shared-flows/start.yaml @@ -22,15 +22,15 @@ env: - runScript: file: ../../scripts/performance/dist/actions/track-action.js env: - ACTION: 'openLink' - TARGET: 'onramp-deeplink' - PHASE: 'start' + ACTION: "openLink" + TARGET: "onramp-deeplink" + PHASE: "start" - openLink: - link: 'uniswap://app/fiatonramp?userAddress=0xEEf806b3Cae8fcecAe1793EE1e0B2c738F61C6bB&source=push' + link: "uniswap://app/fiatonramp?userAddress=0xEEf806b3Cae8fcecAe1793EE1e0B2c738F61C6bB&source=push" autoVerify: true # Handle iOS deeplink permission dialog (optional - only appears on first run) - tapOn: - text: 'Open' + text: "Open" optional: true - waitForAnimationToEnd: timeout: 5000 @@ -41,43 +41,43 @@ env: - runScript: file: ../../scripts/performance/dist/actions/track-action.js env: - ACTION: 'openLink' - TARGET: 'onramp-deeplink' - PHASE: 'end' + ACTION: "openLink" + TARGET: "onramp-deeplink" + PHASE: "end" - killApp # Open widget deeplink - runScript: file: ../../scripts/performance/dist/actions/track-action.js env: - ACTION: 'openLink' - TARGET: 'widget-deeplink' - PHASE: 'start' + ACTION: "openLink" + TARGET: "widget-deeplink" + PHASE: "start" - openLink: - link: 'uniswap://widget/#/tokens/ethereum/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984' + link: "uniswap://widget/#/tokens/ethereum/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984" autoVerify: true - waitForAnimationToEnd: timeout: 3000 - assertVisible: id: ${output.testIds.TokenDetailsHeaderText} - text: 'Uniswap' + text: "Uniswap" - runScript: file: ../../scripts/performance/dist/actions/track-action.js env: - ACTION: 'openLink' - TARGET: 'widget-deeplink' - PHASE: 'end' + ACTION: "openLink" + TARGET: "widget-deeplink" + PHASE: "end" - killApp # Open swap deeplink - runScript: file: ../../scripts/performance/dist/actions/track-action.js env: - ACTION: 'openLink' - TARGET: 'swap-deeplink' - PHASE: 'start' + ACTION: "openLink" + TARGET: "swap-deeplink" + PHASE: "start" - openLink: - link: 'uniswap://redirect?screen=swap&userAddress=0xEEf806b3Cae8fcecAe1793EE1e0B2c738F61C6bB&inputCurrencyId=1-0x6B175474E89094C44Da98b954EedeAC495271d0F&outputCurrencyId=1-0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984¤cyField=input&amount=100' + link: "uniswap://redirect?screen=swap&userAddress=0xEEf806b3Cae8fcecAe1793EE1e0B2c738F61C6bB&inputCurrencyId=1-0x6B175474E89094C44Da98b954EedeAC495271d0F&outputCurrencyId=1-0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984¤cyField=input&amount=100" autoVerify: true - waitForAnimationToEnd: timeout: 3000 @@ -92,20 +92,20 @@ env: - runScript: file: ../../scripts/performance/dist/actions/track-action.js env: - ACTION: 'openLink' - TARGET: 'swap-deeplink' - PHASE: 'end' + ACTION: "openLink" + TARGET: "swap-deeplink" + PHASE: "end" - killApp # Open token details deeplink - runScript: file: ../../scripts/performance/dist/actions/track-action.js env: - ACTION: 'openLink' - TARGET: 'token-details-deeplink' - PHASE: 'start' + ACTION: "openLink" + TARGET: "token-details-deeplink" + PHASE: "start" - openLink: - link: 'uniswap://app/tokendetails?currencyId=10-0x6fd9d7ad17242c41f7131d257212c54a0e816691&source=push' + link: "uniswap://app/tokendetails?currencyId=10-0x6fd9d7ad17242c41f7131d257212c54a0e816691&source=push" autoVerify: true - waitForAnimationToEnd: timeout: 3000 @@ -116,20 +116,20 @@ env: - runScript: file: ../../scripts/performance/dist/actions/track-action.js env: - ACTION: 'openLink' - TARGET: 'token-details-deeplink' - PHASE: 'end' + ACTION: "openLink" + TARGET: "token-details-deeplink" + PHASE: "end" - killApp # Open transaction history deeplink - runScript: file: ../../scripts/performance/dist/actions/track-action.js env: - ACTION: 'openLink' - TARGET: 'transaction-history-deeplink' - PHASE: 'start' + ACTION: "openLink" + TARGET: "transaction-history-deeplink" + PHASE: "start" - openLink: - link: 'uniswap://redirect?screen=transaction&fiatOnRamp=true&userAddress=0xEEf806b3Cae8fcecAe1793EE1e0B2c738F61C6bB' + link: "uniswap://redirect?screen=transaction&fiatOnRamp=true&userAddress=0xEEf806b3Cae8fcecAe1793EE1e0B2c738F61C6bB" autoVerify: true - waitForAnimationToEnd: timeout: 3000 @@ -138,20 +138,20 @@ env: - runScript: file: ../../scripts/performance/dist/actions/track-action.js env: - ACTION: 'openLink' - TARGET: 'transaction-history-deeplink' - PHASE: 'end' + ACTION: "openLink" + TARGET: "transaction-history-deeplink" + PHASE: "end" - killApp # Invalid deeplink (should fail gracefully and remain functional) - runScript: file: ../../scripts/performance/dist/actions/track-action.js env: - ACTION: 'openLink' - TARGET: 'invalid-deeplink' - PHASE: 'start' + ACTION: "openLink" + TARGET: "invalid-deeplink" + PHASE: "start" - openLink: - link: 'uniswap://invalid-path' + link: "uniswap://invalid-path" autoVerify: true - waitForAnimationToEnd: timeout: 3000 @@ -160,46 +160,46 @@ env: - runScript: file: ../../scripts/performance/dist/actions/track-action.js env: - ACTION: 'openLink' - TARGET: 'invalid-deeplink' - PHASE: 'end' + ACTION: "openLink" + TARGET: "invalid-deeplink" + PHASE: "end" - killApp # Open moonpayOnly onramp deeplink - runScript: file: ../../scripts/performance/dist/actions/track-action.js env: - ACTION: 'openLink' - TARGET: 'moonpay-onramp-deeplink' - PHASE: 'start' + ACTION: "openLink" + TARGET: "moonpay-onramp-deeplink" + PHASE: "start" - openLink: - link: 'uniswap://app/fiatonramp?source=push&moonpayOnly=true&moonpayCurrencyCode=usdc&amount=200' + link: "uniswap://app/fiatonramp?source=push&moonpayOnly=true&moonpayCurrencyCode=usdc&amount=200" autoVerify: true - waitForAnimationToEnd: timeout: 5000 - assertVisible: id: ${output.testIds.ForFormTokenSelected} - text: 'USD Coin' + text: "USD Coin" - assertVisible: id: ${output.testIds.BuyFormAmountInput} - text: '200' + text: "200" - runScript: file: ../../scripts/performance/dist/actions/track-action.js env: - ACTION: 'openLink' - TARGET: 'moonpay-onramp-deeplink' - PHASE: 'end' + ACTION: "openLink" + TARGET: "moonpay-onramp-deeplink" + PHASE: "end" - killApp # Open scantastic deeplink (when user scans QR code on the extension) - runScript: file: ../../scripts/performance/dist/actions/track-action.js env: - ACTION: 'openLink' - TARGET: 'scantastic-deeplink' - PHASE: 'start' + ACTION: "openLink" + TARGET: "scantastic-deeplink" + PHASE: "start" - openLink: - link: 'uniswap://scantastic?pubKey=%7B%22alg%22%3A%22RSA-OAEP-256%22%2C%22kty%22%3A%22RSA%22%2C%22n%22%3A%224X4nRAEZ8FWoVmoQ5KrxcssIR7XpdcVo_y7yD1SgmYuXekvHMIYuLxxkxVTjsyxj2s9jctIHOhZ-g96w4oM8-HXjCJG_v55w6FZyDskllcmaGeUlZFwWkiqZ-PKkHCWxCe_dZGvL33sazS_L8P3eAxXEPEJMG9p9lxsIlPp7ki0GSyVjq4rrHgW0lIz6qy6WqHbnyJWQAMSPnZTGM697ZCdkW_GTD3MyqitBwK5xNQN8Pxgbu6S7xbQglanYNBbeMYpJ3X1PDl37sp16YwPm6ryGaX1ESDPHa3M7-_we_yQEUQvtU5t2dd8chISJX8L1D7s8iNxM1LxG_nZTwKnccRPtrzKj-osBMbfCoU4fiNS2LC7q6zsyHxgDpeFlrV--iboQ9TsaQ7RGaFOSKs0l74_dt8GvX2JtNJ0ah8K__eNg9q0xBD8DTdeY2duMTEKJZIKgEyX0KUiRpsbsNmm_76iqhhZyYvcb6mwvNnVcXPg_TabX7lQEEippd7JTWVnF2LKzldlUonchQSsbLEUlN_ALa0Nuq6GG1MVJ0JjSsNMcpin6rH9fPzmDKkqzM2qvhdyuV66vkS82Wj9tQpqXL_jkRk7bQsDlB-HiVbzM2oNPk6or5u6p5tJni0th6BZm4z-sYgmMj3D5xHeusyap-8dmS9J4mXDxGLL_NloaHY8%22%2C%22e%22%3A%22AQAB%22%7D&uuid=28c01911-8e69-46e9-b2f0-f5e719bb714b&vendor=Apple&model=Macintosh&browser=Chrome' + link: "uniswap://scantastic?pubKey=%7B%22alg%22%3A%22RSA-OAEP-256%22%2C%22kty%22%3A%22RSA%22%2C%22n%22%3A%224X4nRAEZ8FWoVmoQ5KrxcssIR7XpdcVo_y7yD1SgmYuXekvHMIYuLxxkxVTjsyxj2s9jctIHOhZ-g96w4oM8-HXjCJG_v55w6FZyDskllcmaGeUlZFwWkiqZ-PKkHCWxCe_dZGvL33sazS_L8P3eAxXEPEJMG9p9lxsIlPp7ki0GSyVjq4rrHgW0lIz6qy6WqHbnyJWQAMSPnZTGM697ZCdkW_GTD3MyqitBwK5xNQN8Pxgbu6S7xbQglanYNBbeMYpJ3X1PDl37sp16YwPm6ryGaX1ESDPHa3M7-_we_yQEUQvtU5t2dd8chISJX8L1D7s8iNxM1LxG_nZTwKnccRPtrzKj-osBMbfCoU4fiNS2LC7q6zsyHxgDpeFlrV--iboQ9TsaQ7RGaFOSKs0l74_dt8GvX2JtNJ0ah8K__eNg9q0xBD8DTdeY2duMTEKJZIKgEyX0KUiRpsbsNmm_76iqhhZyYvcb6mwvNnVcXPg_TabX7lQEEippd7JTWVnF2LKzldlUonchQSsbLEUlN_ALa0Nuq6GG1MVJ0JjSsNMcpin6rH9fPzmDKkqzM2qvhdyuV66vkS82Wj9tQpqXL_jkRk7bQsDlB-HiVbzM2oNPk6or5u6p5tJni0th6BZm4z-sYgmMj3D5xHeusyap-8dmS9J4mXDxGLL_NloaHY8%22%2C%22e%22%3A%22AQAB%22%7D&uuid=28c01911-8e69-46e9-b2f0-f5e719bb714b&vendor=Apple&model=Macintosh&browser=Chrome" autoVerify: true - waitForAnimationToEnd: timeout: 3000 @@ -207,16 +207,16 @@ env: id: ${output.testIds.ScantasticConfirmationTitle} - assertVisible: id: ${output.testIds.ScantasticDevice} - text: 'Apple Macintosh' + text: "Apple Macintosh" - assertVisible: id: ${output.testIds.ScantasticBrowser} - text: 'Chrome' + text: "Chrome" - runScript: file: ../../scripts/performance/dist/actions/track-action.js env: - ACTION: 'openLink' - TARGET: 'scantastic-deeplink' - PHASE: 'end' + ACTION: "openLink" + TARGET: "scantastic-deeplink" + PHASE: "end" - killApp # End flow tracking @@ -228,4 +228,4 @@ env: file: ../../scripts/performance/upload-metrics.js env: DATADOG_API_KEY: ${DATADOG_API_KEY} - ENVIRONMENT: 'maestro_cloud' + ENVIRONMENT: "maestro_cloud" diff --git a/apps/mobile/.maestro/flows/restore/restore-new-device.yaml b/apps/mobile/.maestro/flows/restore/restore-new-device.yaml index 0f73d3201f3..08ab78e29fa 100644 --- a/apps/mobile/.maestro/flows/restore/restore-new-device.yaml +++ b/apps/mobile/.maestro/flows/restore/restore-new-device.yaml @@ -184,10 +184,29 @@ env: PHASE: 'end' - waitForAnimationToEnd +# Wait for cloud backup to fail - handle both possible error states +# First try waiting for "No backups found" - extendedWaitUntil: visible: text: 'No backups found' - timeout: 5000 # wait for cloud backup to fail + timeout: 5000 + optional: true + +# If that didn't appear, wait for "Error while importing backups" +- extendedWaitUntil: + visible: + text: 'Error while importing backups' + timeout: 5000 + optional: true + +# If error while importing backups appeared, tap to enter recovery phrase manually +- runFlow: + when: + visible: + text: 'Enter recovery phrase' + commands: + - tapOn: + text: 'Enter recovery phrase' # Track seed phrase input - runScript: diff --git a/apps/mobile/.maestro/shared-flows/navigate-to-explore.yaml b/apps/mobile/.maestro/shared-flows/navigate-to-explore.yaml index 3431fe0cb24..200f366aeaf 100644 --- a/apps/mobile/.maestro/shared-flows/navigate-to-explore.yaml +++ b/apps/mobile/.maestro/shared-flows/navigate-to-explore.yaml @@ -1,20 +1,37 @@ appId: com.uniswap.mobile.dev --- # This flow handles the common action of navigating to the Explore tab -# from the main wallet screen. +# from the main wallet screen. It supports both bottom tabs navigation (new) +# and the legacy floating navigation bar. # Wait for the home screen to be visible - extendedWaitUntil: - visible: 'noop' + visible: "noop" timeout: 2000 optional: true -# Tap on the Explore/Search button in the navigation bar +# OPTION 1: Try bottom tabs navigation first (new UI pattern) +# This will be available when BottomTabs feature flag is enabled +- tapOn: + id: ${output.testIds.ExploreTab} + optional: true + +# TODO: INFRA-1074 - Remove this fallback when we no longer support the legacy navigation bar +# OPTION 2: Fallback to legacy floating navigation bar +# This will be used when BottomTabs feature flag is disabled - tapOn: id: ${output.testIds.SearchTokensAndWallets} + optional: true +# Wait for tab animations to complete (200ms animation duration) - waitForAnimationToEnd # Verify we're in the Explore screen by checking for the search input +# This verification works for both navigation patterns +- extendedWaitUntil: + visible: + id: ${output.testIds.ExploreSearchInput} + timeout: 3000 + - assertVisible: id: ${output.testIds.ExploreSearchInput} diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 6e581251f19..18f214ecb22 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -111,6 +111,7 @@ "@uniswap/ethers-rs-mobile": "0.0.5", "@uniswap/sdk-core": "7.7.2", "@universe/api": "workspace:^", + "@universe/gating": "workspace:^", "@walletconnect/core": "2.21.4", "@walletconnect/react-native-compat": "2.21.4", "@walletconnect/types": "2.21.4", diff --git a/apps/mobile/src/app/App.tsx b/apps/mobile/src/app/App.tsx index 7554650723a..da7c8559205 100644 --- a/apps/mobile/src/app/App.tsx +++ b/apps/mobile/src/app/App.tsx @@ -3,6 +3,17 @@ import { loadDevMessages, loadErrorMessages } from '@apollo/client/dev' import { DdRum, RumActionType } from '@datadog/mobile-react-native' import { BottomSheetModalProvider } from '@gorhom/bottom-sheet' import { PerformanceProfiler, RenderPassReport } from '@shopify/react-native-performance' +import { + DatadogSessionSampleRateKey, + DynamicConfigs, + Experiments, + getDynamicConfigValue, + getStatsigClient, + StatsigCustomAppValue, + StatsigUser, + Storage, + WALLET_FEATURE_FLAG_NAMES, +} from '@universe/gating' import { MMKVWrapper } from 'apollo3-cache-persist' import { default as React, StrictMode, useCallback, useEffect, useMemo, useRef } from 'react' import { I18nextProvider } from 'react-i18next' @@ -55,13 +66,7 @@ import { BlankUrlProvider } from 'uniswap/src/contexts/UrlContext' import { initializePortfolioQueryOverrides } from 'uniswap/src/data/rest/portfolioBalanceOverrides' import { selectFavoriteTokens } from 'uniswap/src/features/favorites/selectors' import { useAppFiatCurrencyInfo } from 'uniswap/src/features/fiatCurrency/hooks' -import { DatadogSessionSampleRateKey, DynamicConfigs } from 'uniswap/src/features/gating/configs' -import { StatsigCustomAppValue } from 'uniswap/src/features/gating/constants' -import { Experiments } from 'uniswap/src/features/gating/experiments' -import { WALLET_FEATURE_FLAG_NAMES } from 'uniswap/src/features/gating/flags' -import { getDynamicConfigValue } from 'uniswap/src/features/gating/hooks' import { StatsigProviderWrapper } from 'uniswap/src/features/gating/StatsigProviderWrapper' -import { getStatsigClient, StatsigUser, Storage } from 'uniswap/src/features/gating/sdk/statsig' import { useCurrentLanguageInfo } from 'uniswap/src/features/language/hooks' import { LocalizationContextProvider } from 'uniswap/src/features/language/LocalizationContext' import { clearNotificationQueue } from 'uniswap/src/features/notifications/slice/slice' @@ -121,6 +126,8 @@ initDynamicIntlPolyfills() initOneSignal() initAppsFlyer() +initializePortfolioQueryOverrides({ store }) + function App(): JSX.Element | null { useEffect(() => { if (!__DEV__) { @@ -240,12 +247,6 @@ function AppOuter(): JSX.Element | null { } }, []) - useEffect(() => { - if (client) { - initializePortfolioQueryOverrides({ store, apolloClient: client }) - } - }, [client]) - if (!client) { return null } diff --git a/apps/mobile/src/app/MobileWalletNavigationProvider.tsx b/apps/mobile/src/app/MobileWalletNavigationProvider.tsx index 9e692d3fe02..6f2319b4364 100644 --- a/apps/mobile/src/app/MobileWalletNavigationProvider.tsx +++ b/apps/mobile/src/app/MobileWalletNavigationProvider.tsx @@ -1,4 +1,5 @@ import { StackActions } from '@react-navigation/native' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { PropsWithChildren, useCallback } from 'react' import { Share } from 'react-native' import { useDispatch } from 'react-redux' @@ -15,8 +16,6 @@ import { useFiatOnRampAggregatorGetCountryQuery, } from 'uniswap/src/features/fiatOnRamp/api' import { RampDirection } from 'uniswap/src/features/fiatOnRamp/types' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { ModalName, WalletEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { TransactionState } from 'uniswap/src/features/transactions/types/transactionState' diff --git a/apps/mobile/src/app/modals/BridgedAssetWarningWrapper.tsx b/apps/mobile/src/app/modals/BridgedAssetWarningWrapper.tsx index 6b6c3ade50d..29cd1bd00a2 100644 --- a/apps/mobile/src/app/modals/BridgedAssetWarningWrapper.tsx +++ b/apps/mobile/src/app/modals/BridgedAssetWarningWrapper.tsx @@ -1,7 +1,6 @@ import { AppStackScreenProp } from 'src/app/navigation/types' import { useReactNavigationModal } from 'src/components/modals/useReactNavigationModal' import { BridgedAssetModal } from 'uniswap/src/components/BridgedAsset/BridgedAssetModal' -import { checkIsBridgedAsset } from 'uniswap/src/components/BridgedAsset/utils' import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { ModalName } from 'uniswap/src/features/telemetry/constants' import { useDismissedBridgedAssetWarnings } from 'uniswap/src/features/tokens/slice/hooks' @@ -36,7 +35,7 @@ export function BridgedAssetWarningWrapper({ return null } - const isBridgedAsset = checkIsBridgedAsset(currencyInfo) + const isBridgedAsset = Boolean(currencyInfo.isBridged) // If token is not bridged or warning was dismissed and not blocked, skip warning and proceed to SwapFlow if (!isBridgedAsset || bridgedAssetWarningDismissed) { diff --git a/apps/mobile/src/app/navigation/navigation.tsx b/apps/mobile/src/app/navigation/navigation.tsx index d59a3a4d5d1..07cbfafd88c 100644 --- a/apps/mobile/src/app/navigation/navigation.tsx +++ b/apps/mobile/src/app/navigation/navigation.tsx @@ -1,6 +1,7 @@ import { NavigationContainer, NavigationIndependentTree } from '@react-navigation/native' import { createNativeStackNavigator } from '@react-navigation/native-stack' import { createStackNavigator, TransitionPresets } from '@react-navigation/stack' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import React, { useEffect } from 'react' import { DevSettings } from 'react-native' import { INCLUDE_PROTOTYPE_FEATURES, IS_E2E_TEST } from 'react-native-dotenv' @@ -110,8 +111,6 @@ import { ViewPrivateKeysScreen } from 'src/screens/ViewPrivateKeys/ViewPrivateKe import { WebViewScreen } from 'src/screens/WebViewScreen' import { useSporeColors } from 'ui/src' import { spacing } from 'ui/src/theme' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { ModalName } from 'uniswap/src/features/telemetry/constants' import { useAppInsets } from 'uniswap/src/hooks/useAppInsets' import { OnboardingEntryPoint } from 'uniswap/src/types/onboarding' diff --git a/apps/mobile/src/app/navigation/tabs/CustomTabBar/CustomTabBar.tsx b/apps/mobile/src/app/navigation/tabs/CustomTabBar/CustomTabBar.tsx index dc6727df6f5..01de66c7fe1 100644 --- a/apps/mobile/src/app/navigation/tabs/CustomTabBar/CustomTabBar.tsx +++ b/apps/mobile/src/app/navigation/tabs/CustomTabBar/CustomTabBar.tsx @@ -37,6 +37,7 @@ const TabItem = ({ tab, index, isFocused, onPress, colors }: TabItemProps): JSX. return ( numberOfDigits: PriceNumberOfDigits spotPrice?: SharedValue + startingPrice?: number + shouldTreatAsStablecoin?: boolean } const PriceTextSection = memo(function PriceTextSection({ loading, numberOfDigits, spotPrice, + startingPrice, + shouldTreatAsStablecoin, }: PriceTextProps): JSX.Element { const price = useLineChartPrice(spotPrice) const currency = useAppFiatCurrencyInfo() @@ -62,9 +66,13 @@ const PriceTextSection = memo(function PriceTextSection({ We want both the animated number skeleton and the relative change skeleton to hide at the exact same time. When multiple skeletons hide in different order, it gives the feeling of things being slower than they actually are. */} - - + + ) }) @@ -144,7 +152,7 @@ const PriceExplorerInner = memo(function _PriceExplorerInner(): JSX.Element { value: convertedSpotValue, } ) - }, [data, convertedSpotValue]) + }, [data]) // Zoom out y-axis for low variance assets const shouldZoomOut = useMemo(() => { @@ -177,6 +185,9 @@ const PriceExplorerInner = memo(function _PriceExplorerInner(): JSX.Element { return } + // Get the starting price for fiat delta calculation + const startingPrice = convertedPriceHistory[0]?.value + return ( @@ -185,6 +196,8 @@ const PriceExplorerInner = memo(function _PriceExplorerInner(): JSX.Element { numberOfDigits={numberOfDigits} relativeChange={convertedSpot?.relativeChange} spotPrice={convertedSpot?.value} + startingPrice={startingPrice} + shouldTreatAsStablecoin={shouldZoomOut} /> diff --git a/apps/mobile/src/components/PriceExplorer/Text.tsx b/apps/mobile/src/components/PriceExplorer/Text.tsx index 0da167fcdcd..8cddaffd8e7 100644 --- a/apps/mobile/src/components/PriceExplorer/Text.tsx +++ b/apps/mobile/src/components/PriceExplorer/Text.tsx @@ -1,7 +1,8 @@ import React from 'react' -import { useAnimatedStyle } from 'react-native-reanimated' +import { useAnimatedStyle, useDerivedValue } from 'react-native-reanimated' import { useLineChartDatetime } from 'react-native-wagmi-charts' import { AnimatedDecimalNumber } from 'src/components/PriceExplorer/AnimatedDecimalNumber' +import { useLineChartFiatDelta } from 'src/components/PriceExplorer/useFiatDelta' import { useLineChartPrice, useLineChartRelativeChange } from 'src/components/PriceExplorer/usePrice' import { AnimatedText } from 'src/components/text/AnimatedText' import { Flex, Text, useSporeColors } from 'ui/src' @@ -39,19 +40,47 @@ export function PriceText({ maxWidth }: { loading: boolean; maxWidth?: number }) ) } -export function RelativeChangeText({ loading }: { loading: boolean }): JSX.Element { +export function RelativeChangeText({ + loading, + startingPrice, + shouldTreatAsStablecoin = false, +}: { + loading: boolean + startingPrice?: number + shouldTreatAsStablecoin?: boolean +}): JSX.Element { const colors = useSporeColors() const relativeChange = useLineChartRelativeChange() + const fiatDelta = useLineChartFiatDelta({ + startingPrice, + shouldTreatAsStablecoin, + }) + + const changeColor = useDerivedValue(() => { + if (relativeChange.value.value === 0) { + return colors.neutral3.val + } + return relativeChange.value.value > 0 ? colors.statusSuccess.val : colors.statusCritical.val + }) const styles = useAnimatedStyle(() => ({ - color: relativeChange.value.value >= 0 ? colors.statusSuccess.val : colors.statusCritical.val, + color: changeColor.value, })) const caretStyle = useAnimatedStyle(() => ({ - color: relativeChange.value.value >= 0 ? colors.statusSuccess.val : colors.statusCritical.val, + color: changeColor.value, transform: [{ rotate: relativeChange.value.value >= 0 ? '180deg' : '0deg' }], })) + // Combine fiat delta and percentage in a derived value + const combinedText = useDerivedValue(() => { + const delta = fiatDelta.formatted.value + if (delta) { + return `${delta} (${relativeChange.formatted.value})` + } + return relativeChange.formatted.value + }) + return ( = 0 ? -1 : 1 }, ]} /> - + )} @@ -93,8 +122,8 @@ export function DatetimeText({ loading }: { loading: boolean }): JSX.Element | n } return ( - - + + ) } diff --git a/apps/mobile/src/components/PriceExplorer/__snapshots__/Text.test.tsx.snap b/apps/mobile/src/components/PriceExplorer/__snapshots__/Text.test.tsx.snap index 98694cc5409..4b14e906407 100644 --- a/apps/mobile/src/components/PriceExplorer/__snapshots__/Text.test.tsx.snap +++ b/apps/mobile/src/components/PriceExplorer/__snapshots__/Text.test.tsx.snap @@ -6,10 +6,9 @@ exports[`DatetimeText renders without error 1`] = ` @@ -32,9 +31,9 @@ exports[`DatetimeText renders without error 1`] = ` }, }, "fontFamily": "Basel Grotesk", - "fontSize": 19, + "fontSize": 15, "fontWeight": "400", - "lineHeight": 24.7, + "lineHeight": 19.5, "padding": 0, } } diff --git a/apps/mobile/src/components/PriceExplorer/useFiatDelta.tsx b/apps/mobile/src/components/PriceExplorer/useFiatDelta.tsx new file mode 100644 index 00000000000..ff0024319e8 --- /dev/null +++ b/apps/mobile/src/components/PriceExplorer/useFiatDelta.tsx @@ -0,0 +1,116 @@ +import { useCallback, useMemo } from 'react' +import { runOnJS, SharedValue, useAnimatedReaction, useDerivedValue, useSharedValue } from 'react-native-reanimated' +import { useLineChart } from 'react-native-wagmi-charts' +import { useFormatChartFiatDelta } from 'uniswap/src/features/fiatCurrency/hooks/useFormatChartFiatDelta' +import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext' + +interface UseFiatDeltaParams { + startingPrice?: number + shouldTreatAsStablecoin?: boolean +} + +interface FiatDeltaResult { + formatted: SharedValue +} + +/** + * Hook to calculate and format fiat delta for price charts. + * Optimized to only calculate deltas on-demand during scrubbing, reducing memory usage. + */ +export function useLineChartFiatDelta({ + startingPrice, + shouldTreatAsStablecoin = false, +}: UseFiatDeltaParams): FiatDeltaResult { + const { currentIndex, data, isActive } = useLineChart() + const { conversionRate } = useLocalizationContext() + const { formatChartFiatDelta } = useFormatChartFiatDelta() + + // Shared value for the current scrubbing delta + const scrubbingDeltaSharedValue = useSharedValue('') + + // Pre-calculate only the last point's delta (for non-scrubbing state) + const lastPointDelta = useMemo(() => { + if (!startingPrice || !data || !conversionRate || data.length === 0) { + return '' + } + + const convertedStartPrice = startingPrice * conversionRate + const lastPoint = data[data.length - 1] + if (!lastPoint) { + return '' + } + const convertedEndPrice = lastPoint.value * conversionRate + + const delta = formatChartFiatDelta({ + startingPrice: convertedStartPrice, + endingPrice: convertedEndPrice, + isStablecoin: shouldTreatAsStablecoin, + }) + + return delta.formatted + }, [startingPrice, data, conversionRate, formatChartFiatDelta, shouldTreatAsStablecoin]) + + // Calculate delta for current scrubbing position + const calculateCurrentDelta = useMemo(() => { + return (index: number) => { + if (!startingPrice || !data || !conversionRate) { + return '' + } + + const currentPoint = data[index] + if (!currentPoint) { + return '' + } + + const convertedStartPrice = startingPrice * conversionRate + const convertedEndPrice = currentPoint.value * conversionRate + + const delta = formatChartFiatDelta({ + startingPrice: convertedStartPrice, + endingPrice: convertedEndPrice, + isStablecoin: shouldTreatAsStablecoin, + }) + + return delta.formatted + } + }, [startingPrice, data, conversionRate, formatChartFiatDelta, shouldTreatAsStablecoin]) + + // Callback for updating the scrubbing delta from the UI thread + const updateScrubbingDelta = useCallback( + (index: number) => { + scrubbingDeltaSharedValue.value = calculateCurrentDelta(index) + }, + [calculateCurrentDelta], + ) + + // Track current index changes with useAnimatedReaction + useAnimatedReaction( + () => { + return currentIndex.value + }, + (currentIndexValue) => { + if (data && data.length > 0) { + const safeIndex = Math.min(Math.max(0, Math.round(currentIndexValue)), data.length - 1) + runOnJS(updateScrubbingDelta)(safeIndex) + } + }, + [data, updateScrubbingDelta], + ) + + // Create a derived value that decides which delta to show + const formatted = useDerivedValue(() => { + if (!data || data.length === 0) { + return '' + } + + // When scrubbing, use the current scrubbing delta + if (isActive.value) { + return scrubbingDeltaSharedValue.value + } + + // When not scrubbing, use the pre-calculated last point delta + return lastPointDelta + }) + + return { formatted } +} diff --git a/apps/mobile/src/components/PriceExplorer/usePrice.tsx b/apps/mobile/src/components/PriceExplorer/usePrice.tsx index 3b8197599c2..0e23b3d110b 100644 --- a/apps/mobile/src/components/PriceExplorer/usePrice.tsx +++ b/apps/mobile/src/components/PriceExplorer/usePrice.tsx @@ -68,7 +68,7 @@ export function useLineChartPrice(currentSpot?: SharedValue): ValueAndFo formatted: priceFormatted, shouldAnimate, }), - [price, priceFormatted, shouldAnimate], + [], ) } diff --git a/apps/mobile/src/components/PriceExplorer/usePriceHistory.ts b/apps/mobile/src/components/PriceExplorer/usePriceHistory.ts index 5f1222aaeda..62775d74aee 100644 --- a/apps/mobile/src/components/PriceExplorer/usePriceHistory.ts +++ b/apps/mobile/src/components/PriceExplorer/usePriceHistory.ts @@ -83,7 +83,7 @@ export function useTokenPriceHistory({ relativeChange: spotRelativeChange, } : undefined, - [price, spotValue, spotRelativeChange], + [price], ) const formattedPriceHistory = useMemo(() => { diff --git a/apps/mobile/src/components/Requests/ModalWithOverlay/ModalWithOverlay.tsx b/apps/mobile/src/components/Requests/ModalWithOverlay/ModalWithOverlay.tsx index e86b139e12b..4ea51ca8353 100644 --- a/apps/mobile/src/components/Requests/ModalWithOverlay/ModalWithOverlay.tsx +++ b/apps/mobile/src/components/Requests/ModalWithOverlay/ModalWithOverlay.tsx @@ -1,4 +1,5 @@ import { BottomSheetFooter, BottomSheetScrollView, useBottomSheetInternal } from '@gorhom/bottom-sheet' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { PropsWithChildren, useCallback, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { @@ -17,8 +18,6 @@ import { Button, ButtonProps, Flex } from 'ui/src' import { spacing } from 'ui/src/theme' import { Modal } from 'uniswap/src/components/modals/Modal' import { ModalProps } from 'uniswap/src/components/modals/ModalProps' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { useAppInsets } from 'uniswap/src/hooks/useAppInsets' import { TestID } from 'uniswap/src/test/fixtures/testIDs' diff --git a/apps/mobile/src/components/Requests/RequestModal/WalletConnectRequestModal.tsx b/apps/mobile/src/components/Requests/RequestModal/WalletConnectRequestModal.tsx index daf85557387..768181e6026 100644 --- a/apps/mobile/src/components/Requests/RequestModal/WalletConnectRequestModal.tsx +++ b/apps/mobile/src/components/Requests/RequestModal/WalletConnectRequestModal.tsx @@ -1,4 +1,5 @@ import { useNetInfo } from '@react-native-community/netinfo' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { getSdkError } from '@walletconnect/utils' import { providers } from 'ethers' import React, { useMemo, useRef } from 'react' @@ -29,8 +30,6 @@ import { spacing } from 'ui/src/theme' import { EthMethod } from 'uniswap/src/features/dappRequests/types' import { isSignTypedDataRequest } from 'uniswap/src/features/dappRequests/utils' import { useTransactionGasFee } from 'uniswap/src/features/gas/hooks' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { Platform } from 'uniswap/src/features/platforms/types/Platform' import { useHasAccountMismatchCallback } from 'uniswap/src/features/smartWallet/mismatch/hooks' import { MobileEventName, ModalName } from 'uniswap/src/features/telemetry/constants' diff --git a/apps/mobile/src/components/Requests/ScanSheet/WalletConnectModal.tsx b/apps/mobile/src/components/Requests/ScanSheet/WalletConnectModal.tsx index b3d79b23c5c..d3bd8eb86a0 100644 --- a/apps/mobile/src/components/Requests/ScanSheet/WalletConnectModal.tsx +++ b/apps/mobile/src/components/Requests/ScanSheet/WalletConnectModal.tsx @@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { Alert } from 'react-native' import 'react-native-reanimated' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { useDispatch } from 'react-redux' import { useEagerExternalProfileRootNavigation } from 'src/app/navigation/hooks' import { BackButtonView } from 'src/components/layout/BackButtonView' @@ -24,8 +25,6 @@ import { Modal } from 'uniswap/src/components/modals/Modal' import { ScannerModalState } from 'uniswap/src/components/ReceiveQRCode/constants' import { ReceiveQRCode } from 'uniswap/src/components/ReceiveQRCode/ReceiveQRCode' import { AccountType } from 'uniswap/src/features/accounts/types' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants' import Trace from 'uniswap/src/features/telemetry/Trace' import { TestID } from 'uniswap/src/test/fixtures/testIDs' diff --git a/apps/mobile/src/components/Requests/ScanSheet/util.test.ts b/apps/mobile/src/components/Requests/ScanSheet/util.test.ts index 441ed9a01a7..76993011636 100644 --- a/apps/mobile/src/components/Requests/ScanSheet/util.test.ts +++ b/apps/mobile/src/components/Requests/ScanSheet/util.test.ts @@ -1,5 +1,9 @@ import * as wcUtils from '@walletconnect/utils' import { CUSTOM_UNI_QR_CODE_PREFIX, getSupportedURI, URIType } from 'src/components/Requests/ScanSheet/util' +import { + UNISWAP_URL_SCHEME_WALLETCONNECT_AS_PARAM, + UNISWAP_WALLETCONNECT_URL, +} from 'src/features/deepLinking/constants' import { wcAsParamInUniwapScheme, wcInUniwapScheme, @@ -121,4 +125,84 @@ describe('getSupportedURI', () => { it('should return undefined for invalid metamask address', async () => { expect(await getSupportedURI('ethereum:invalid_address')).toBeUndefined() }) + + describe('URL and HTML encoded URIs', () => { + it('should handle percent-encoded WalletConnect v2 URI', async () => { + // Simulate a URI that has been percent-encoded (& becomes %26) + const encodedUri = encodeURIComponent(VALID_WC_V2_URI) + const result = await getSupportedURI(encodedUri) + expect(result).toEqual({ type: URIType.WalletConnectV2URL, value: VALID_WC_V2_URI }) + }) + + it('should handle HTML entity-encoded ampersands in WalletConnect v2 URI', async () => { + // Simulate a URI with HTML-encoded ampersands (& becomes &) + const htmlEncodedUri = VALID_WC_V2_URI.replace(/&/g, '&') + const result = await getSupportedURI(htmlEncodedUri) + expect(result).toEqual({ type: URIType.WalletConnectV2URL, value: VALID_WC_V2_URI }) + }) + + it('should handle both percent-encoded and HTML entity-encoded URI', async () => { + // First apply HTML encoding, then percent encoding + const htmlEncodedUri = VALID_WC_V2_URI.replace(/&/g, '&') + const doubleEncodedUri = encodeURIComponent(htmlEncodedUri) + const result = await getSupportedURI(doubleEncodedUri) + expect(result).toEqual({ type: URIType.WalletConnectV2URL, value: VALID_WC_V2_URI }) + }) + + it('should handle percent-encoded uniswap scheme URI with query param', async () => { + const fullUri = UNISWAP_URL_SCHEME_WALLETCONNECT_AS_PARAM + VALID_WC_V2_URI + const encodedUri = encodeURIComponent(fullUri) + const result = await getSupportedURI(encodedUri) + expect(result).toEqual({ type: URIType.WalletConnectV2URL, value: VALID_WC_V2_URI }) + }) + + it('should handle HTML entity-encoded uniswap scheme URI with query param', async () => { + const fullUri = UNISWAP_URL_SCHEME_WALLETCONNECT_AS_PARAM + VALID_WC_V2_URI + const htmlEncodedUri = fullUri.replace(/&/g, '&') + const result = await getSupportedURI(htmlEncodedUri) + expect(result).toEqual({ type: URIType.WalletConnectV2URL, value: VALID_WC_V2_URI }) + }) + + it('should handle percent-encoded uniswap app URL', async () => { + const fullUri = UNISWAP_WALLETCONNECT_URL + VALID_WC_V2_URI + const encodedUri = encodeURIComponent(fullUri) + const result = await getSupportedURI(encodedUri) + expect(result).toEqual({ type: URIType.WalletConnectV2URL, value: VALID_WC_V2_URI }) + }) + + it('should handle HTML entity-encoded uniswap app URL', async () => { + const fullUri = UNISWAP_WALLETCONNECT_URL + VALID_WC_V2_URI + const htmlEncodedUri = fullUri.replace(/&/g, '&') + const result = await getSupportedURI(htmlEncodedUri) + expect(result).toEqual({ type: URIType.WalletConnectV2URL, value: VALID_WC_V2_URI }) + }) + + it('should handle percent-encoded hello_uniwallet prefix', async () => { + const fullUri = CUSTOM_UNI_QR_CODE_PREFIX + VALID_WC_V2_URI + const encodedUri = encodeURIComponent(fullUri) + const result = await getSupportedURI(encodedUri) + expect(result).toEqual({ type: URIType.WalletConnectV2URL, value: VALID_WC_V2_URI }) + }) + + it('should handle HTML entity-encoded hello_uniwallet prefix', async () => { + const fullUri = CUSTOM_UNI_QR_CODE_PREFIX + VALID_WC_V2_URI + const htmlEncodedUri = fullUri.replace(/&/g, '&') + const result = await getSupportedURI(htmlEncodedUri) + expect(result).toEqual({ type: URIType.WalletConnectV2URL, value: VALID_WC_V2_URI }) + }) + + it('should handle malformed percent-encoded URI without crashing', async () => { + // Malformed URI with invalid percent encoding (% not followed by valid hex) + const malformedUri = 'uniswap://wc?uri=%E0%A4%A' + // Should not throw an error, even with malformed encoding + await expect(getSupportedURI(malformedUri)).resolves.not.toThrow() + }) + + it('should handle URI with standalone percent sign', async () => { + // URI with a standalone % which is invalid for decodeURIComponent + const malformedUri = 'hello_uniwallet:' + VALID_WC_V2_URI + '%' + // Should not throw an error, even with malformed encoding + await expect(getSupportedURI(malformedUri)).resolves.not.toThrow() + }) + }) }) diff --git a/apps/mobile/src/components/Requests/ScanSheet/util.ts b/apps/mobile/src/components/Requests/ScanSheet/util.ts index 140882c7d38..f8f07334473 100644 --- a/apps/mobile/src/components/Requests/ScanSheet/util.ts +++ b/apps/mobile/src/components/Requests/ScanSheet/util.ts @@ -46,6 +46,9 @@ export async function getSupportedURI( return undefined } + // Decode URI in case it's encoded (handles both percent encoding and HTML ampersand) + uri = safeDecodeURIComponent(uri).replace(/&/g, '&') + const maybeAddress = getValidAddress({ address: uri, platform: Platform.EVM, @@ -72,7 +75,7 @@ export async function getSupportedURI( (await getWcUriWithCustomPrefix(uri, CUSTOM_UNI_QR_CODE_PREFIX)) || (await getWcUriWithCustomPrefix(uri, UNISWAP_URL_SCHEME_WALLETCONNECT_AS_PARAM)) || (await getWcUriWithCustomPrefix(uri, UNISWAP_URL_SCHEME)) || - (await getWcUriWithCustomPrefix(decodeURIComponent(uri), UNISWAP_WALLETCONNECT_URL)) || + (await getWcUriWithCustomPrefix(uri, UNISWAP_WALLETCONNECT_URL)) || {} if (maybeCustomWcUri && type) { @@ -147,6 +150,22 @@ export function getScantasticQueryParams(uri: string): Nullable { return uriParts[1] || null } +function safeDecodeURIComponent(value: string): string { + try { + return decodeURIComponent(value) + } catch (e) { + logger.error(new Error('Failed to decode URI component'), { + tags: { + file: 'util.ts', + function: 'safeDecodeURIComponent', + }, + extra: { value, error: e }, + }) + // If decoding fails, return the original value + return value + } +} + const PARAM_PUB_KEY = 'pubKey' const PARAM_UUID = 'uuid' const PARAM_VENDOR = 'vendor' @@ -181,10 +200,10 @@ export function parseScantasticParams(uri: string): ScantasticParams | undefined try { return ScantasticParamsSchema.parse({ publicKey: publicKey ? JSON.parse(publicKey) : undefined, - uuid: uuid ? decodeURIComponent(uuid) : undefined, - vendor: vendor ? decodeURIComponent(vendor) : undefined, - model: model ? decodeURIComponent(model) : undefined, - browser: browser ? decodeURIComponent(browser) : undefined, + uuid: uuid ? safeDecodeURIComponent(uuid) : undefined, + vendor: vendor ? safeDecodeURIComponent(vendor) : undefined, + model: model ? safeDecodeURIComponent(model) : undefined, + browser: browser ? safeDecodeURIComponent(browser) : undefined, }) } catch (e) { const wrappedError = new Error('Invalid scantastic params', { cause: e }) diff --git a/apps/mobile/src/components/Requests/Uwulink/utils.ts b/apps/mobile/src/components/Requests/Uwulink/utils.ts index a600453c6dc..5fede7fe5ea 100644 --- a/apps/mobile/src/components/Requests/Uwulink/utils.ts +++ b/apps/mobile/src/components/Requests/Uwulink/utils.ts @@ -1,15 +1,15 @@ -import { parseEther } from 'ethers/lib/utils' -import { WalletConnectSigningRequest } from 'src/features/walletConnect/walletConnectSlice' -import { AssetType } from 'uniswap/src/entities/assets' -import { SignerMnemonicAccountMeta } from 'uniswap/src/features/accounts/types' -import { EthMethod } from 'uniswap/src/features/dappRequests/types' import { DynamicConfigs, UwULinkAllowlist, UwULinkAllowlistItem, UwuLinkConfigKey, -} from 'uniswap/src/features/gating/configs' -import { useDynamicConfigValue } from 'uniswap/src/features/gating/hooks' + useDynamicConfigValue, +} from '@universe/gating' +import { parseEther } from 'ethers/lib/utils' +import { WalletConnectSigningRequest } from 'src/features/walletConnect/walletConnectSlice' +import { AssetType } from 'uniswap/src/entities/assets' +import { SignerMnemonicAccountMeta } from 'uniswap/src/features/accounts/types' +import { EthMethod } from 'uniswap/src/features/dappRequests/types' import { isUwULinkAllowlistType } from 'uniswap/src/features/gating/typeGuards' import { DappRequestType, diff --git a/apps/mobile/src/components/TokenDetails/TokenDetailsBridgedAssetSection.tsx b/apps/mobile/src/components/TokenDetails/TokenDetailsBridgedAssetSection.tsx index 840d9c10050..cd3aecd60b1 100644 --- a/apps/mobile/src/components/TokenDetails/TokenDetailsBridgedAssetSection.tsx +++ b/apps/mobile/src/components/TokenDetails/TokenDetailsBridgedAssetSection.tsx @@ -1,8 +1,6 @@ -import { useMemo } from 'react' import { navigate } from 'src/app/navigation/rootNavigation' import { useTokenDetailsContext } from 'src/components/TokenDetails/TokenDetailsContext' import { BridgedAssetTDPSection } from 'uniswap/src/components/BridgedAsset/BridgedAssetTDPSection' -import { checkIsBridgedAsset } from 'uniswap/src/components/BridgedAsset/utils' import { ModalName } from 'uniswap/src/features/telemetry/constants' import { useCurrencyInfo } from 'uniswap/src/features/tokens/useCurrencyInfo' import { CurrencyField } from 'uniswap/src/types/currency' @@ -29,7 +27,7 @@ export function TokenDetailsBridgedAssetSection(): JSX.Element | null { }, }) }) - const isBridgedAsset = useMemo(() => currencyInfo && checkIsBridgedAsset(currencyInfo), [currencyInfo]) + const isBridgedAsset = Boolean(currencyInfo?.isBridged) if (!isBridgedAsset || !currencyInfo) { return null } diff --git a/apps/mobile/src/components/accounts/AccountHeader.tsx b/apps/mobile/src/components/accounts/AccountHeader.tsx index 040684b16b8..5b29965af69 100644 --- a/apps/mobile/src/components/accounts/AccountHeader.tsx +++ b/apps/mobile/src/components/accounts/AccountHeader.tsx @@ -1,4 +1,5 @@ import { SharedEventName } from '@uniswap/analytics-events' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import React, { useCallback, useEffect } from 'react' import { Gesture, GestureDetector, State } from 'react-native-gesture-handler' import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withDelay, withTiming } from 'react-native-reanimated' @@ -11,8 +12,6 @@ import { CopyAlt, ScanHome, SettingsHome } from 'ui/src/components/icons' import { ScannerModalState } from 'uniswap/src/components/ReceiveQRCode/constants' import { AccountIcon } from 'uniswap/src/features/accounts/AccountIcon' import { AccountType, DisplayNameType } from 'uniswap/src/features/accounts/types' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { pushNotification } from 'uniswap/src/features/notifications/slice/slice' import { AppNotificationType, CopyNotificationType } from 'uniswap/src/features/notifications/slice/types' import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants' @@ -39,7 +38,7 @@ const RotatingSettingsIcon = ({ onPressSettings }: { onPressSettings(): void }): if (isScreenFocused) { pressProgress.value = withDelay(50, withTiming(0)) } - }, [isScreenFocused, pressProgress]) + }, [isScreenFocused]) const tap = Gesture.Tap() .withTestId(TestID.AccountHeaderSettings) diff --git a/apps/mobile/src/components/activity/ActivityContent.tsx b/apps/mobile/src/components/activity/ActivityContent.tsx index bdbab3bf204..ed12c5ecaaa 100644 --- a/apps/mobile/src/components/activity/ActivityContent.tsx +++ b/apps/mobile/src/components/activity/ActivityContent.tsx @@ -1,6 +1,7 @@ import type { LegendListRef } from '@legendapp/list' import { LegendList } from '@legendapp/list' import { useScrollToTop } from '@react-navigation/native' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import type { ForwardedRef } from 'react' import { forwardRef, memo, useMemo, useRef } from 'react' import type { FlatList } from 'react-native' @@ -17,14 +18,13 @@ import { openModal } from 'src/features/modals/modalSlice' import { removePendingSession } from 'src/features/walletConnect/walletConnectSlice' import { Flex, useSporeColors } from 'ui/src' import { ScannerModalState } from 'uniswap/src/components/ReceiveQRCode/constants' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { ModalName } from 'uniswap/src/features/telemetry/constants' import { useAppInsets } from 'uniswap/src/hooks/useAppInsets' import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { DDRumManualTiming } from 'utilities/src/logger/datadog/datadogEvents' import { usePerformanceLogger } from 'utilities/src/logger/usePerformanceLogger' import { isAndroid } from 'utilities/src/platform' +import { useEvent } from 'utilities/src/react/hooks' import { useActivityDataWallet } from 'wallet/src/features/activity/useActivityDataWallet' const ESTIMATED_ITEM_SIZE = 92 @@ -55,11 +55,11 @@ export const ActivityContent = memo( const { onContentSizeChange, adaptiveFooter } = useAdaptiveFooter(containerProps?.contentContainerStyle) - const onPressReceive = (): void => { + const onPressReceive = useEvent((): void => { // in case we received a pending session from a previous scan after closing modal dispatch(removePendingSession()) dispatch(openModal({ name: ModalName.WalletConnectScan, initialState: ScannerModalState.WalletQr })) - } + }) const { maybeEmptyComponent, renderActivityItem, sectionData, keyExtractor } = useActivityDataWallet({ evmOwner: owner, @@ -105,6 +105,9 @@ export const ActivityContent = memo( ListEmptyComponent={maybeEmptyComponent} ListFooterComponent={isExternalProfile ? null : adaptiveFooter} contentContainerStyle={containerProps?.contentContainerStyle} + refreshControl={refreshControl} + refreshing={refreshing} + onContentSizeChange={onContentSizeChange} /> ) : ( { // @ts-expect-error https://github.com/software-mansion/react-native-reanimated/issues/2976 myRef.current?._listRef._scrollRef.scrollTo({ x: Math.floor(scroll.value / fullWidth - 0.5) * fullWidth, }) - }, [fullWidth, scroll]) + }, [fullWidth]) return ( diff --git a/apps/mobile/src/components/explore/ExploreSections/ExploreSections.tsx b/apps/mobile/src/components/explore/ExploreSections/ExploreSections.tsx index 26d19b1252f..edda8bccac5 100644 --- a/apps/mobile/src/components/explore/ExploreSections/ExploreSections.tsx +++ b/apps/mobile/src/components/explore/ExploreSections/ExploreSections.tsx @@ -6,6 +6,7 @@ import { TokenStats, } from '@uniswap/client-explore/dist/uniswap/explore/v1/service_pb' import { ALL_NETWORKS_ARG } from '@universe/api' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { @@ -34,8 +35,6 @@ import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard' import { useTokenRankingsQuery } from 'uniswap/src/data/rest/tokenRankings' import type { UniverseChainId } from 'uniswap/src/features/chains/types' import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { MobileEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { useAppInsets } from 'uniswap/src/hooks/useAppInsets' diff --git a/apps/mobile/src/components/explore/search/ExploreScreenSearchResultsList.tsx b/apps/mobile/src/components/explore/search/ExploreScreenSearchResultsList.tsx index 1ae943b09fb..49db351bbc5 100644 --- a/apps/mobile/src/components/explore/search/ExploreScreenSearchResultsList.tsx +++ b/apps/mobile/src/components/explore/search/ExploreScreenSearchResultsList.tsx @@ -1,3 +1,4 @@ +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { memo, useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { KeyboardAvoidingView } from 'react-native-keyboard-controller' @@ -5,8 +6,6 @@ import { ESTIMATED_BOTTOM_TABS_HEIGHT } from 'src/app/navigation/tabs/CustomTabB import { Flex, flexStyles, Text, TouchableArea } from 'ui/src' import { spacing } from 'ui/src/theme' import type { UniverseChainId } from 'uniswap/src/features/chains/types' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { SearchModalNoQueryList } from 'uniswap/src/features/search/SearchModal/SearchModalNoQueryList' import { SearchModalResultsList } from 'uniswap/src/features/search/SearchModal/SearchModalResultsList' import { MOBILE_SEARCH_TABS, SearchTab } from 'uniswap/src/features/search/SearchModal/types' diff --git a/apps/mobile/src/components/home/HomeExploreTab.tsx b/apps/mobile/src/components/home/HomeExploreTab.tsx index a52b9881913..899a6fddaed 100644 --- a/apps/mobile/src/components/home/HomeExploreTab.tsx +++ b/apps/mobile/src/components/home/HomeExploreTab.tsx @@ -1,5 +1,6 @@ import { ReactNavigationPerformanceView } from '@shopify/react-native-performance-navigation' import { GraphQLApi } from '@universe/api' +import { DynamicConfigs, HomeScreenExploreTokensConfigKey, useDynamicConfigValue } from '@universe/gating' import { ForwardedRef, forwardRef, memo, useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { FlatList, LayoutRectangle, RefreshControl } from 'react-native' @@ -15,8 +16,6 @@ import { SwirlyArrowDown } from 'ui/src/components/icons' import { spacing, zIndexes } from 'ui/src/theme' import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' import { useAppFiatCurrency } from 'uniswap/src/features/fiatCurrency/hooks' -import { DynamicConfigs, HomeScreenExploreTokensConfigKey } from 'uniswap/src/features/gating/configs' -import { useDynamicConfigValue } from 'uniswap/src/features/gating/hooks' import { isContractInputArrayType } from 'uniswap/src/features/gating/typeGuards' import { MobileEventName } from 'uniswap/src/features/telemetry/constants' import { useAppInsets } from 'uniswap/src/hooks/useAppInsets' diff --git a/apps/mobile/src/components/home/TokensTab.tsx b/apps/mobile/src/components/home/TokensTab.tsx index 9b41db908a6..b5da1f62cb5 100644 --- a/apps/mobile/src/components/home/TokensTab.tsx +++ b/apps/mobile/src/components/home/TokensTab.tsx @@ -1,4 +1,5 @@ import { useStartProfiler } from '@shopify/react-native-performance' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import React, { forwardRef, memo, useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { FlatList } from 'react-native' @@ -14,8 +15,6 @@ import { NoTokens } from 'ui/src/components/icons' import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard' import { PortfolioEmptyState } from 'uniswap/src/components/portfolio/PortfolioEmptyState' import { ScannerModalState } from 'uniswap/src/components/ReceiveQRCode/constants' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { TokenBalanceListRow } from 'uniswap/src/features/portfolio/types' import { ModalName } from 'uniswap/src/features/telemetry/constants' import { CurrencyId } from 'uniswap/src/types/currency' diff --git a/apps/mobile/src/components/home/hooks.tsx b/apps/mobile/src/components/home/hooks.tsx index c216431ac85..e5b8826f637 100644 --- a/apps/mobile/src/components/home/hooks.tsx +++ b/apps/mobile/src/components/home/hooks.tsx @@ -1,6 +1,8 @@ +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { useCallback, useEffect, useMemo } from 'react' import { StyleProp, ViewStyle } from 'react-native' import Animated, { SharedValue, useAnimatedStyle, useSharedValue } from 'react-native-reanimated' +import { ESTIMATED_BOTTOM_TABS_HEIGHT } from 'src/app/navigation/tabs/CustomTabBar/constants' import { TAB_BAR_HEIGHT } from 'src/components/layout/TabHelpers' import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions' import { useAppInsets } from 'uniswap/src/hooks/useAppInsets' @@ -13,6 +15,9 @@ export function useAdaptiveFooter(contentContainerStyle?: StyleProp): } { const { fullHeight } = useDeviceDimensions() const insets = useAppInsets() + const isBottomTabsEnabled = useFeatureFlag(FeatureFlags.BottomTabs) + + const HEIGHT_TO_SUBTRACT = isBottomTabsEnabled ? ESTIMATED_BOTTOM_TABS_HEIGHT : TAB_BAR_HEIGHT // Content is rendered under the navigation bar but not under the status bar const maxContentHeight = fullHeight - insets.top // Use maxContentHeight as the initial value to properly position the TabBar @@ -28,7 +33,7 @@ export function useAdaptiveFooter(contentContainerStyle?: StyleProp): } // The height of the footer added to the list can be calculated from // the following equation (for collapsed tab bar): - // maxContentHeight = TAB_BAR_HEIGHT + + footerHeight + paddingBottom + // maxContentHeight = HEIGHT_TO_SUBTRACT + + footerHeight + paddingBottom // // To get the we need to subtract padding already // added to the content container style and the footer if it's already @@ -36,17 +41,17 @@ export function useAdaptiveFooter(contentContainerStyle?: StyleProp): // = contentHeight - paddingTop - paddingBottom - footerHeight // // The resulting equation is: - // footerHeight = maxContentHeight - - TAB_BAR_HEIGHT - paddingBottom - // = maxContentHeight - (contentHeight - paddingTop - paddingBottom - footerHeight) - TAB_BAR_HEIGHT - paddingBottom - // = maxContentHeight + paddingTop + footerHeight - (contentHeight + TAB_BAR_HEIGHT) + // footerHeight = maxContentHeight - - HEIGHT_TO_SUBTRACT - paddingBottom + // = maxContentHeight - (contentHeight - paddingTop - paddingBottom - footerHeight) - HEIGHT_TO_SUBTRACT - paddingBottom + // = maxContentHeight + paddingTop + footerHeight - (contentHeight + HEIGHT_TO_SUBTRACT) const paddingTopProp = (contentContainerStyle as ViewStyle).paddingTop const paddingTop = typeof paddingTopProp === 'number' ? paddingTopProp : 0 const calculatedFooterHeight = - maxContentHeight + paddingTop + footerHeight.value - (contentHeight + TAB_BAR_HEIGHT) + maxContentHeight + paddingTop + footerHeight.value - (contentHeight + HEIGHT_TO_SUBTRACT) footerHeight.value = Math.max(0, calculatedFooterHeight) }, - [footerHeight, contentContainerStyle, maxContentHeight], + [contentContainerStyle, maxContentHeight, HEIGHT_TO_SUBTRACT], ) // biome-ignore lint/correctness/useExhaustiveDependencies: we want to recalculate this when activeAccount changes diff --git a/apps/mobile/src/components/home/introCards/FundWalletModal.tsx b/apps/mobile/src/components/home/introCards/FundWalletModal.tsx index 67565c140cf..35c592bf7ee 100644 --- a/apps/mobile/src/components/home/introCards/FundWalletModal.tsx +++ b/apps/mobile/src/components/home/introCards/FundWalletModal.tsx @@ -1,3 +1,4 @@ +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import React, { PropsWithChildren, useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { FlatList } from 'react-native' @@ -13,8 +14,6 @@ import { ActionCard, ActionCardItem } from 'uniswap/src/components/misc/ActionCa import { Modal } from 'uniswap/src/components/modals/Modal' import { ImageUri } from 'uniswap/src/components/nfts/images/ImageUri' import { useCexTransferProviders } from 'uniswap/src/features/fiatOnRamp/useCexTransferProviders' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants' import { usePortfolioEmptyStateBackground } from 'wallet/src/components/portfolio/empty' diff --git a/apps/mobile/src/components/home/introCards/OnboardingIntroCardStack.tsx b/apps/mobile/src/components/home/introCards/OnboardingIntroCardStack.tsx index a03ea0a749d..910a539088f 100644 --- a/apps/mobile/src/components/home/introCards/OnboardingIntroCardStack.tsx +++ b/apps/mobile/src/components/home/introCards/OnboardingIntroCardStack.tsx @@ -1,4 +1,5 @@ import { SharedEventName } from '@uniswap/analytics-events' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import React, { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' @@ -12,8 +13,6 @@ import { BRIDGED_ASSETS_CARD_BANNER, PUSH_NOTIFICATIONS_CARD_BANNER } from 'ui/s import { Buy } from 'ui/src/components/icons' import { AccountType } from 'uniswap/src/features/accounts/types' import { UniverseChainId } from 'uniswap/src/features/chains/types' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { ElementName, ModalName, WalletEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { OnboardingCardLoggingName } from 'uniswap/src/features/telemetry/types' diff --git a/apps/mobile/src/components/layout/TabHelpers.tsx b/apps/mobile/src/components/layout/TabHelpers.tsx index 3cbea4bdbc6..e09383d79f3 100644 --- a/apps/mobile/src/components/layout/TabHelpers.tsx +++ b/apps/mobile/src/components/layout/TabHelpers.tsx @@ -1,5 +1,6 @@ /* eslint-disable react-native/no-unused-styles */ import { FlashList, FlashListProps } from '@shopify/flash-list' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import React, { RefObject, useCallback, useMemo } from 'react' import { FlatList, @@ -14,8 +15,6 @@ import Animated, { SharedValue } from 'react-native-reanimated' import { Route } from 'react-native-tab-view' import { Flex, Text } from 'ui/src' import { colorsLight, spacing } from 'ui/src/theme' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { TestIDType } from 'uniswap/src/test/fixtures/testIDs' import { PendingNotificationBadge } from 'wallet/src/features/notifications/components/PendingNotificationBadge' diff --git a/apps/mobile/src/components/loading/parts/WaveLoader.tsx b/apps/mobile/src/components/loading/parts/WaveLoader.tsx index 8eb6baa7881..202eb9da63b 100644 --- a/apps/mobile/src/components/loading/parts/WaveLoader.tsx +++ b/apps/mobile/src/components/loading/parts/WaveLoader.tsx @@ -19,7 +19,6 @@ export function WaveLoader(): JSX.Element { const yPosition = useSharedValue(0) const { chartHeight } = useChartDimensions() - // biome-ignore lint/correctness/useExhaustiveDependencies: only want to do this once on mount useEffect(() => { yPosition.value = withRepeat(withTiming(1, { duration: WAVE_DURATION }), Infinity, false) }, []) diff --git a/apps/mobile/src/features/datadog/DatadogProviderWrapper.tsx b/apps/mobile/src/features/datadog/DatadogProviderWrapper.tsx index b307753f982..33cc7e78ee6 100644 --- a/apps/mobile/src/features/datadog/DatadogProviderWrapper.tsx +++ b/apps/mobile/src/features/datadog/DatadogProviderWrapper.tsx @@ -8,15 +8,15 @@ import { UploadFrequency, } from '@datadog/mobile-react-native' import { ErrorEventMapper } from '@datadog/mobile-react-native/lib/typescript/rum/eventMappers/errorEventMapper' -import { PropsWithChildren, default as React, useEffect, useState } from 'react' -import { DatadogContext } from 'src/features/datadog/DatadogContext' -import { config } from 'uniswap/src/config' import { DatadogIgnoredErrorsConfigKey, DatadogIgnoredErrorsValType, DynamicConfigs, -} from 'uniswap/src/features/gating/configs' -import { getDynamicConfigValue } from 'uniswap/src/features/gating/hooks' + getDynamicConfigValue, +} from '@universe/gating' +import { PropsWithChildren, default as React, useEffect, useState } from 'react' +import { DatadogContext } from 'src/features/datadog/DatadogContext' +import { config } from 'uniswap/src/config' import { datadogEnabledBuild, isTestRun, localDevDatadogEnabled } from 'utilities/src/environment/constants' import { setAttributesToDatadog } from 'utilities/src/logger/datadog/Datadog' import { getDatadogEnvironment } from 'utilities/src/logger/datadog/env' diff --git a/apps/mobile/src/features/deepLinking/configUtils.ts b/apps/mobile/src/features/deepLinking/configUtils.ts index f6047a337dd..19231b35d75 100644 --- a/apps/mobile/src/features/deepLinking/configUtils.ts +++ b/apps/mobile/src/features/deepLinking/configUtils.ts @@ -2,10 +2,10 @@ import { DeepLinkUrlAllowlist, DeepLinkUrlAllowlistConfigKey, DynamicConfigs, + getDynamicConfigValue, UwULinkAllowlist, UwuLinkConfigKey, -} from 'uniswap/src/features/gating/configs' -import { getDynamicConfigValue } from 'uniswap/src/features/gating/hooks' +} from '@universe/gating' import { isUwULinkAllowlistType } from 'uniswap/src/features/gating/typeGuards' /** diff --git a/apps/mobile/src/features/deepLinking/deepLinkUtils.ts b/apps/mobile/src/features/deepLinking/deepLinkUtils.ts index a25138e2060..318a9e14845 100644 --- a/apps/mobile/src/features/deepLinking/deepLinkUtils.ts +++ b/apps/mobile/src/features/deepLinking/deepLinkUtils.ts @@ -1,3 +1,4 @@ +import { DeepLinkUrlAllowlist } from '@universe/gating' import { getScantasticQueryParams } from 'src/components/Requests/ScanSheet/util' import { UNISWAP_URL_SCHEME_UWU_LINK } from 'src/components/Requests/Uwulink/utils' import { getInAppBrowserAllowlist } from 'src/features/deepLinking/configUtils' @@ -8,7 +9,6 @@ import { UNISWAP_WALLETCONNECT_URL, } from 'src/features/deepLinking/constants' import { UNISWAP_WEB_HOSTNAME } from 'uniswap/src/constants/urls' -import { DeepLinkUrlAllowlist } from 'uniswap/src/features/gating/configs' import { isCurrencyIdValid } from 'uniswap/src/utils/currencyId' import { logger } from 'utilities/src/logger/logger' diff --git a/apps/mobile/src/features/deepLinking/handleDeepLinkSaga.test.ts b/apps/mobile/src/features/deepLinking/handleDeepLinkSaga.test.ts index 170a0955994..9424abf457a 100644 --- a/apps/mobile/src/features/deepLinking/handleDeepLinkSaga.test.ts +++ b/apps/mobile/src/features/deepLinking/handleDeepLinkSaga.test.ts @@ -42,13 +42,11 @@ jest.mock('expo-web-browser', () => ({ FULL_SCREEN: 'fullScreen', }, })) -jest.mock('uniswap/src/features/gating/sdk/statsig', () => ({ +jest.mock('@universe/gating', () => ({ + ...jest.requireActual('@universe/gating'), getStatsigClient: jest.fn(() => ({ checkGate: jest.fn(() => false), // Always return false to avoid Korea gate redirects })), -})) - -jest.mock('uniswap/src/features/gating/hooks', () => ({ getFeatureFlag: jest.fn(() => false), // Default to false for feature flags })) diff --git a/apps/mobile/src/features/deepLinking/handleDeepLinkSaga.ts b/apps/mobile/src/features/deepLinking/handleDeepLinkSaga.ts index a9afe70a1a0..c115585ae5b 100644 --- a/apps/mobile/src/features/deepLinking/handleDeepLinkSaga.ts +++ b/apps/mobile/src/features/deepLinking/handleDeepLinkSaga.ts @@ -1,4 +1,5 @@ import { createAction } from '@reduxjs/toolkit' +import { FeatureFlags, getFeatureFlagName, getStatsigClient } from '@universe/gating' import { parseUri } from '@walletconnect/utils' import { Alert } from 'react-native' import { navigate } from 'src/app/navigation/rootNavigation' @@ -29,8 +30,6 @@ import { waitForWcWeb3WalletIsReady } from 'src/features/walletConnect/walletCon import { addRequest, setDidOpenFromDeepLink } from 'src/features/walletConnect/walletConnectSlice' import { call, delay, put, select, takeLatest } from 'typed-redux-saga' import { AccountType } from 'uniswap/src/features/accounts/types' -import { FeatureFlags, getFeatureFlagName } from 'uniswap/src/features/gating/flags' -import { getStatsigClient } from 'uniswap/src/features/gating/sdk/statsig' import { MobileEventName, ModalName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import i18n from 'uniswap/src/i18n' diff --git a/apps/mobile/src/features/deepLinking/handleOnRampReturnLinkSaga.ts b/apps/mobile/src/features/deepLinking/handleOnRampReturnLinkSaga.ts index da904ede2ea..8edff2d8379 100644 --- a/apps/mobile/src/features/deepLinking/handleOnRampReturnLinkSaga.ts +++ b/apps/mobile/src/features/deepLinking/handleOnRampReturnLinkSaga.ts @@ -1,9 +1,8 @@ +import { FeatureFlags, getFeatureFlag } from '@universe/gating' import { navigate } from 'src/app/navigation/rootNavigation' import { HomeScreenTabIndex } from 'src/screens/HomeScreen/HomeScreenTabIndex' import { dismissInAppBrowser } from 'src/utils/linking' import { call, put } from 'typed-redux-saga' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { getFeatureFlag } from 'uniswap/src/features/gating/hooks' import { forceFetchFiatOnRampTransactions } from 'uniswap/src/features/transactions/slice' import { MobileScreens } from 'uniswap/src/types/screens/mobile' diff --git a/apps/mobile/src/features/deepLinking/handleTransactionLinkSaga.ts b/apps/mobile/src/features/deepLinking/handleTransactionLinkSaga.ts index fefafa0b2dd..c6e5d16b175 100644 --- a/apps/mobile/src/features/deepLinking/handleTransactionLinkSaga.ts +++ b/apps/mobile/src/features/deepLinking/handleTransactionLinkSaga.ts @@ -1,9 +1,8 @@ +import { FeatureFlags, getFeatureFlag } from '@universe/gating' import { navigate } from 'src/app/navigation/rootNavigation' import { closeAllModals } from 'src/features/modals/modalSlice' import { HomeScreenTabIndex } from 'src/screens/HomeScreen/HomeScreenTabIndex' import { call, put } from 'typed-redux-saga' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { getFeatureFlag } from 'uniswap/src/features/gating/hooks' import { MobileScreens } from 'uniswap/src/types/screens/mobile' export function* handleTransactionLink() { diff --git a/apps/mobile/src/features/lockScreen/LockScreenModal.tsx b/apps/mobile/src/features/lockScreen/LockScreenModal.tsx index e1c19214daf..91addda367a 100644 --- a/apps/mobile/src/features/lockScreen/LockScreenModal.tsx +++ b/apps/mobile/src/features/lockScreen/LockScreenModal.tsx @@ -1,3 +1,4 @@ +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { BlurView } from 'expo-blur' import React, { memo } from 'react' import { useTranslation } from 'react-i18next' @@ -15,8 +16,6 @@ import { UNISWAP_MONO_LOGO_LARGE } from 'ui/src/assets' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions' import { spacing, zIndexes } from 'ui/src/theme' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { useAppInsets } from 'uniswap/src/hooks/useAppInsets' import { isAndroid } from 'utilities/src/platform' import { useEvent } from 'utilities/src/react/hooks' diff --git a/apps/mobile/src/features/send/SendFormButton.tsx b/apps/mobile/src/features/send/SendFormButton.tsx index 2b7019008c5..82bf9ea84db 100644 --- a/apps/mobile/src/features/send/SendFormButton.tsx +++ b/apps/mobile/src/features/send/SendFormButton.tsx @@ -2,7 +2,6 @@ import React, { Dispatch, SetStateAction, useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' import { Button, Flex } from 'ui/src' -import { checkIsBridgedAsset } from 'uniswap/src/components/BridgedAsset/utils' import { WarningLabel } from 'uniswap/src/components/modals/WarningModal/types' import { nativeOnChain } from 'uniswap/src/constants/tokens' import { AccountType } from 'uniswap/src/features/accounts/types' @@ -60,7 +59,7 @@ export function SendFormButton({ const { tokenWarningDismissed: isCompatibleAddressDismissed } = useDismissedCompatibleAddressWarnings( currencyInInfo?.currency, ) - const isUnichainBridgedAsset = checkIsBridgedAsset(currencyInInfo ?? undefined) && !isCompatibleAddressDismissed + const isUnichainBridgedAsset = Boolean(currencyInInfo?.isBridged) && !isCompatibleAddressDismissed const insufficientGasFunds = warnings.warnings.some((warning) => warning.type === WarningLabel.InsufficientGasFunds) diff --git a/apps/mobile/src/features/wallet/useWalletRestore.ts b/apps/mobile/src/features/wallet/useWalletRestore.ts index 00c8d30154b..d0e4e6214aa 100644 --- a/apps/mobile/src/features/wallet/useWalletRestore.ts +++ b/apps/mobile/src/features/wallet/useWalletRestore.ts @@ -1,9 +1,8 @@ import { useFocusEffect } from '@react-navigation/core' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { useCallback, useEffect, useRef, useState } from 'react' import { navigate } from 'src/app/navigation/rootNavigation' import { WalletRestoreType } from 'src/components/RestoreWalletModal/RestoreWalletModalState' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { ModalName } from 'uniswap/src/features/telemetry/constants' import { logger } from 'utilities/src/logger/logger' import { useSignerAccounts } from 'wallet/src/features/wallet/hooks' diff --git a/apps/mobile/src/features/walletConnect/batchedTransactionSaga.ts b/apps/mobile/src/features/walletConnect/batchedTransactionSaga.ts index 15cd9d3e14b..ae825ac1a3d 100644 --- a/apps/mobile/src/features/walletConnect/batchedTransactionSaga.ts +++ b/apps/mobile/src/features/walletConnect/batchedTransactionSaga.ts @@ -1,4 +1,5 @@ import { TradingApi } from '@universe/api' +import { FeatureFlags, getFeatureFlag } from '@universe/gating' import { getInternalError, getSdkError } from '@walletconnect/utils' import { navigate } from 'src/app/navigation/rootNavigation' import { wcWeb3Wallet } from 'src/features/walletConnect/walletConnectClient' @@ -7,8 +8,6 @@ import { call, put, select } from 'typed-redux-saga' import { UNISWAP_DELEGATION_ADDRESS } from 'uniswap/src/constants/addresses' import { checkWalletDelegation, TradingApiClient } from 'uniswap/src/data/apiClients/tradingApi/TradingApiClient' import { UniverseChainId } from 'uniswap/src/features/chains/types' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { getFeatureFlag } from 'uniswap/src/features/gating/hooks' import { Platform } from 'uniswap/src/features/platforms/types/Platform' import { getEnabledChainIdsSaga } from 'uniswap/src/features/settings/saga' import { ModalName } from 'uniswap/src/features/telemetry/constants' diff --git a/apps/mobile/src/features/walletConnect/saga.ts b/apps/mobile/src/features/walletConnect/saga.ts index ed6d1af97bb..b52b1b8d603 100644 --- a/apps/mobile/src/features/walletConnect/saga.ts +++ b/apps/mobile/src/features/walletConnect/saga.ts @@ -1,6 +1,7 @@ /* eslint-disable max-lines */ import { AnyAction } from '@reduxjs/toolkit' import { WalletKitTypes } from '@reown/walletkit' +import { FeatureFlags, getFeatureFlag } from '@universe/gating' import { PendingRequestTypes, ProposalTypes, SessionTypes, Verify } from '@walletconnect/types' import { buildApprovedNamespaces, getSdkError, populateAuthPayload } from '@walletconnect/utils' import { Alert } from 'react-native' @@ -39,8 +40,6 @@ import { UniverseChainId } from 'uniswap/src/features/chains/types' import { getChainLabel } from 'uniswap/src/features/chains/utils' import { EthMethod } from 'uniswap/src/features/dappRequests/types' import { isSelfCallWithData } from 'uniswap/src/features/dappRequests/utils' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { getFeatureFlag } from 'uniswap/src/features/gating/hooks' import { pushNotification } from 'uniswap/src/features/notifications/slice/slice' import { AppNotificationType } from 'uniswap/src/features/notifications/slice/types' import { Platform } from 'uniswap/src/features/platforms/types/Platform' diff --git a/apps/mobile/src/screens/ActivityScreen.tsx b/apps/mobile/src/screens/ActivityScreen.tsx index d3005fa1c06..5ab318ab1fa 100644 --- a/apps/mobile/src/screens/ActivityScreen.tsx +++ b/apps/mobile/src/screens/ActivityScreen.tsx @@ -2,16 +2,16 @@ import { useApolloClient } from '@apollo/client' import { useScrollToTop } from '@react-navigation/native' import { useQuery } from '@tanstack/react-query' import { GQLQueries } from '@universe/api' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { useCallback, useEffect, useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' import { useDispatch } from 'react-redux' import { ESTIMATED_BOTTOM_TABS_HEIGHT } from 'src/app/navigation/tabs/CustomTabBar/constants' import { ActivityContent } from 'src/components/activity/ActivityContent' import { Screen } from 'src/components/layout/Screen' +import { useAppStateTrigger } from 'src/utils/useAppStateTrigger' import { Text } from 'ui/src' import { spacing } from 'ui/src/theme' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { useSelectAddressHasNotifications } from 'uniswap/src/features/notifications/slice/hooks' import { setNotificationStatus } from 'uniswap/src/features/notifications/slice/slice' import { useAppInsets } from 'uniswap/src/hooks/useAppInsets' @@ -49,6 +49,13 @@ export function ActivityScreen(): JSX.Element { const { refreshing, onRefreshActivityData } = useRefreshActivityData(activeAccount.address) + // Automatically refresh activity data when app comes to foreground + useAppStateTrigger({ + from: 'background', + to: 'active', + callback: onRefreshActivityData, + }) + const insets = useAppInsets() const isBottomTabsEnabled = useFeatureFlag(FeatureFlags.BottomTabs) diff --git a/apps/mobile/src/screens/AppLoadingScreen.tsx b/apps/mobile/src/screens/AppLoadingScreen.tsx index 27192763892..10f97edf4aa 100644 --- a/apps/mobile/src/screens/AppLoadingScreen.tsx +++ b/apps/mobile/src/screens/AppLoadingScreen.tsx @@ -1,4 +1,5 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack' +import { DynamicConfigs, OnDeviceRecoveryConfigKey, useDynamicConfigValue } from '@universe/gating' import dayjs from 'dayjs' import { isEnrolledAsync } from 'expo-local-authentication' import { useCallback, useEffect, useState } from 'react' @@ -15,8 +16,6 @@ import { import { useHideSplashScreen } from 'src/features/splashScreen/useHideSplashScreen' import { RecoveryWalletInfo, useOnDeviceRecoveryData } from 'src/screens/Import/useOnDeviceRecoveryData' import { AccountType } from 'uniswap/src/features/accounts/types' -import { DynamicConfigs, OnDeviceRecoveryConfigKey } from 'uniswap/src/features/gating/configs' -import { useDynamicConfigValue } from 'uniswap/src/features/gating/hooks' import { MobileEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import Trace from 'uniswap/src/features/telemetry/Trace' diff --git a/apps/mobile/src/screens/ExploreScreen.tsx b/apps/mobile/src/screens/ExploreScreen.tsx index ff0cbf76986..136fc7bcaa6 100644 --- a/apps/mobile/src/screens/ExploreScreen.tsx +++ b/apps/mobile/src/screens/ExploreScreen.tsx @@ -1,5 +1,6 @@ import { useIsFocused, useNavigation, useScrollToTop } from '@react-navigation/native' import { SharedEventName } from '@uniswap/analytics-events' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { type TextInput } from 'react-native' @@ -16,8 +17,6 @@ import { HandleBar } from 'uniswap/src/components/modals/HandleBar' import { NetworkFilter, type NetworkFilterProps } from 'uniswap/src/components/network/NetworkFilter' import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import type { UniverseChainId } from 'uniswap/src/features/chains/types' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { useFilterCallbacks } from 'uniswap/src/features/search/SearchModal/hooks/useFilterCallbacks' import { CancelBehaviorType, SearchTextInput } from 'uniswap/src/features/search/SearchTextInput' import { MobileEventName, ModalName, SectionName } from 'uniswap/src/features/telemetry/constants' @@ -98,7 +97,7 @@ export function ExploreScreen(): JSX.Element { }) return unsubscribe - }, [isBottomTabsEnabled, navigation, listRef]) + }, [isBottomTabsEnabled, navigation]) // TODO(WALL-5482): investigate list rendering performance/scrolling issue const canRenderList = useRenderNextFrame(isSheetReady && !isSearchMode) diff --git a/apps/mobile/src/screens/HomeScreen/HomeScreen.tsx b/apps/mobile/src/screens/HomeScreen/HomeScreen.tsx index c57c24a05d1..e9d87ab7df3 100644 --- a/apps/mobile/src/screens/HomeScreen/HomeScreen.tsx +++ b/apps/mobile/src/screens/HomeScreen/HomeScreen.tsx @@ -2,6 +2,7 @@ import { useApolloClient } from '@apollo/client' import { useIsFocused, useScrollToTop } from '@react-navigation/native' import { SharedQueryClient } from '@universe/api' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Freeze } from 'react-freeze' import { useTranslation } from 'react-i18next' @@ -52,8 +53,6 @@ import { NFTS_TAB_DATA_DEPENDENCIES } from 'uniswap/src/components/nfts/constant import { getPortfolioQuery } from 'uniswap/src/data/rest/getPortfolio' import { getListTransactionsQuery } from 'uniswap/src/data/rest/listTransactions' import { AccountType } from 'uniswap/src/features/accounts/types' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { useSelectAddressHasNotifications } from 'uniswap/src/features/notifications/slice/hooks' import { setNotificationStatus } from 'uniswap/src/features/notifications/slice/slice' import { PortfolioBalance } from 'uniswap/src/features/portfolio/PortfolioBalance/PortfolioBalance' diff --git a/apps/mobile/src/screens/HomeScreen/HomeScreenQuickActions.tsx b/apps/mobile/src/screens/HomeScreen/HomeScreenQuickActions.tsx index 30cf4f84917..44b5ac900e0 100644 --- a/apps/mobile/src/screens/HomeScreen/HomeScreenQuickActions.tsx +++ b/apps/mobile/src/screens/HomeScreen/HomeScreenQuickActions.tsx @@ -1,3 +1,4 @@ +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import React, { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { FlatList, ListRenderItemInfo } from 'react-native' @@ -10,8 +11,6 @@ import { ArrowDownCircle, Bank, SendAction, SwapDotted } from 'ui/src/components import { iconSizes, spacing } from 'ui/src/theme' import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { useHighestBalanceNativeCurrencyId } from 'uniswap/src/features/dataApi/balances/balances' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { useHapticFeedback } from 'uniswap/src/features/settings/useHapticFeedback/useHapticFeedback' import { ElementName, MobileEventName, ModalName } from 'uniswap/src/features/telemetry/constants' import { Trace } from 'uniswap/src/features/telemetry/Trace' diff --git a/apps/mobile/src/screens/Import/ImportMethodScreen.tsx b/apps/mobile/src/screens/Import/ImportMethodScreen.tsx index 572ca36bbcc..fe00d212b5d 100644 --- a/apps/mobile/src/screens/Import/ImportMethodScreen.tsx +++ b/apps/mobile/src/screens/Import/ImportMethodScreen.tsx @@ -1,4 +1,5 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import React, { useState } from 'react' import { useTranslation } from 'react-i18next' import { navigate } from 'src/app/navigation/rootNavigation' @@ -17,8 +18,6 @@ import { Flex, SpinningLoader, Text, TouchableArea } from 'ui/src' import { Eye, WalletFilled } from 'ui/src/components/icons' import { useIsDarkMode } from 'ui/src/hooks/useIsDarkMode' import { iconSizes } from 'ui/src/theme' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { authenticateWithPasskeyForSeedPhraseExport } from 'uniswap/src/features/passkey/embeddedWallet' import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants' import Trace from 'uniswap/src/features/telemetry/Trace' diff --git a/apps/mobile/src/screens/Import/OnDeviceRecoveryScreen.tsx b/apps/mobile/src/screens/Import/OnDeviceRecoveryScreen.tsx index 95c14285751..0172fcfffc9 100644 --- a/apps/mobile/src/screens/Import/OnDeviceRecoveryScreen.tsx +++ b/apps/mobile/src/screens/Import/OnDeviceRecoveryScreen.tsx @@ -1,6 +1,7 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack' import { ReactNavigationPerformanceView } from '@shopify/react-native-performance-navigation' import { SharedEventName } from '@uniswap/analytics-events' +import { DynamicConfigs, OnDeviceRecoveryConfigKey, useDynamicConfigValue } from '@universe/gating' import dayjs from 'dayjs' import React, { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -20,8 +21,6 @@ import { iconSizes } from 'ui/src/theme' import { WarningSeverity } from 'uniswap/src/components/modals/WarningModal/types' import { WarningModal } from 'uniswap/src/components/modals/WarningModal/WarningModal' import { AccountType } from 'uniswap/src/features/accounts/types' -import { DynamicConfigs, OnDeviceRecoveryConfigKey } from 'uniswap/src/features/gating/configs' -import { useDynamicConfigValue } from 'uniswap/src/features/gating/hooks' import { Platform } from 'uniswap/src/features/platforms/types/Platform' import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' diff --git a/apps/mobile/src/screens/Import/RestoreMethodScreen.tsx b/apps/mobile/src/screens/Import/RestoreMethodScreen.tsx index 994f0ce214b..d4b12c53b99 100644 --- a/apps/mobile/src/screens/Import/RestoreMethodScreen.tsx +++ b/apps/mobile/src/screens/Import/RestoreMethodScreen.tsx @@ -1,4 +1,5 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import React from 'react' import { useTranslation } from 'react-i18next' import { OnboardingStackParamList } from 'src/app/navigation/types' @@ -13,8 +14,6 @@ import { useNavigationHeader } from 'src/utils/useNavigationHeader' import { Flex, Text, TouchableArea } from 'ui/src' import { WalletFilled } from 'ui/src/components/icons' import { useIsDarkMode } from 'ui/src/hooks/useIsDarkMode' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants' import Trace from 'uniswap/src/features/telemetry/Trace' import { TestID } from 'uniswap/src/test/fixtures/testIDs' diff --git a/apps/mobile/src/screens/Import/SelectWalletScreen.tsx b/apps/mobile/src/screens/Import/SelectWalletScreen.tsx index 8e0f065ad69..a661b53a19c 100644 --- a/apps/mobile/src/screens/Import/SelectWalletScreen.tsx +++ b/apps/mobile/src/screens/Import/SelectWalletScreen.tsx @@ -1,4 +1,5 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import React, { ComponentProps, useCallback } from 'react' import { Trans, useTranslation } from 'react-i18next' import { ScrollView } from 'react-native' @@ -10,8 +11,6 @@ import { Button, Flex, Loader, Text, TouchableArea, useLayoutAnimationOnChange } import { WalletFilled } from 'ui/src/components/icons' import { spacing } from 'ui/src/theme' import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants' import Trace from 'uniswap/src/features/telemetry/Trace' import { TestID } from 'uniswap/src/test/fixtures/testIDs' diff --git a/apps/mobile/src/screens/Onboarding/LandingScreen.tsx b/apps/mobile/src/screens/Onboarding/LandingScreen.tsx index e18fd5f0ac5..d4871955da3 100644 --- a/apps/mobile/src/screens/Onboarding/LandingScreen.tsx +++ b/apps/mobile/src/screens/Onboarding/LandingScreen.tsx @@ -1,5 +1,6 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack' import { ReactNavigationPerformanceView } from '@shopify/react-native-performance-navigation' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import React, { useCallback, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { useAnimatedStyle, useSharedValue, withDelay, withTiming } from 'react-native-reanimated' @@ -12,8 +13,6 @@ import { TermsOfService } from 'src/screens/Onboarding/TermsOfService' import { Button, Flex, Text, TouchableArea } from 'ui/src' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { setIsTestnetModeEnabled } from 'uniswap/src/features/settings/slice' import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants' import Trace from 'uniswap/src/features/telemetry/Trace' @@ -40,7 +39,7 @@ export function LandingScreen({ navigation }: Props): JSX.Element { useEffect(() => { // disables looping animation during e2e tests which was preventing js thread from idle actionButtonsOpacity.value = withDelay(LANDING_ANIMATION_DURATION, withTiming(1, { duration: ONE_SECOND_MS })) - }, [actionButtonsOpacity]) + }, []) // Disables testnet mode on mount if enabled (eg upon removing a wallet) useEffect(() => { diff --git a/apps/mobile/src/screens/SettingsScreen.tsx b/apps/mobile/src/screens/SettingsScreen.tsx index 87db6bed817..8f4d3e83b9c 100644 --- a/apps/mobile/src/screens/SettingsScreen.tsx +++ b/apps/mobile/src/screens/SettingsScreen.tsx @@ -1,4 +1,5 @@ import { useNavigation } from '@react-navigation/core' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { default as React, useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { ListRenderItemInfo } from 'react-native' @@ -56,8 +57,6 @@ import { iconSizes } from 'ui/src/theme' import { uniswapUrls } from 'uniswap/src/constants/urls' import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { useAppFiatCurrencyInfo } from 'uniswap/src/features/fiatCurrency/hooks' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { useCurrentLanguageInfo } from 'uniswap/src/features/language/hooks' import { setIsTestnetModeEnabled } from 'uniswap/src/features/settings/slice' import { useHapticFeedback } from 'uniswap/src/features/settings/useHapticFeedback/useHapticFeedback' diff --git a/apps/mobile/src/screens/TokenDetailsScreen.tsx b/apps/mobile/src/screens/TokenDetailsScreen.tsx index 37413162e96..83a20f7ec10 100644 --- a/apps/mobile/src/screens/TokenDetailsScreen.tsx +++ b/apps/mobile/src/screens/TokenDetailsScreen.tsx @@ -25,7 +25,6 @@ import { Flex, Separator } from 'ui/src' import { ArrowDownCircle, ArrowUpCircle, Bank, SendRoundedAirplane } from 'ui/src/components/icons' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard' -import { getBridgedAsset } from 'uniswap/src/components/BridgedAsset/utils' import type { MenuOptionItem } from 'uniswap/src/components/menus/ContextMenuV2' import { PollingInterval } from 'uniswap/src/constants/misc' import { useCrossChainBalances } from 'uniswap/src/data/balances/hooks/useCrossChainBalances' @@ -296,7 +295,7 @@ const TokenDetailsActionButtonsWrapper = memo(function _TokenDetailsActionButton }, 300) // delay is needed to prevent menu from not closing properly }, [currencyInfo]) - const bridgedAsset = getBridgedAsset(currencyInfo) + const bridgedWithdrawalInfo = currencyInfo?.bridgedWithdrawalInfo const isScreenNavigationReady = useIsScreenNavigationReady({ navigation }) @@ -320,12 +319,12 @@ const TokenDetailsActionButtonsWrapper = memo(function _TokenDetailsActionButton actions.push({ label: t('common.button.buy'), Icon: Bank, onPress: () => onPressBuyFiatOnRamp() }) } - if (!!bridgedAsset && hasTokenBalance) { + if (bridgedWithdrawalInfo && hasTokenBalance) { actions.push({ label: t('common.withdraw'), Icon: ArrowUpCircle, onPress: () => onPressWithdraw(), - subheader: t('bridgedAsset.wormhole.toNativeChain', { nativeChainName: bridgedAsset.nativeChain }), + subheader: t('bridgedAsset.wormhole.toNativeChain', { nativeChainName: bridgedWithdrawalInfo.chain }), actionType: 'external-link', height: 56, }) @@ -346,7 +345,7 @@ const TokenDetailsActionButtonsWrapper = memo(function _TokenDetailsActionButton }, [ fiatOnRampCurrency, t, - bridgedAsset, + bridgedWithdrawalInfo, hasTokenBalance, onPressWithdraw, onPressSend, diff --git a/apps/mobile/src/screens/ViewPrivateKeys/ViewPrivateKeysScreen.tsx b/apps/mobile/src/screens/ViewPrivateKeys/ViewPrivateKeysScreen.tsx index 618d1ae0c4d..5d5bf6a3765 100644 --- a/apps/mobile/src/screens/ViewPrivateKeys/ViewPrivateKeysScreen.tsx +++ b/apps/mobile/src/screens/ViewPrivateKeys/ViewPrivateKeysScreen.tsx @@ -1,5 +1,6 @@ import { CommonActions } from '@react-navigation/core' import { NativeStackScreenProps } from '@react-navigation/native-stack' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import React, { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { ScrollView } from 'react-native-gesture-handler' @@ -17,8 +18,6 @@ import { AlertTriangleFilled } from 'ui/src/components/icons/AlertTriangleFilled import { HiddenWordView } from 'ui/src/components/placeholders/HiddenWordView' import { AddressDisplay } from 'uniswap/src/components/accounts/AddressDisplay' import { Modal } from 'uniswap/src/components/modals/Modal' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants' import { Trace } from 'uniswap/src/features/telemetry/Trace' import { TestID } from 'uniswap/src/test/fixtures/testIDs' diff --git a/apps/mobile/tsconfig.json b/apps/mobile/tsconfig.json index 0f4eef55ad2..94f3a6d681f 100644 --- a/apps/mobile/tsconfig.json +++ b/apps/mobile/tsconfig.json @@ -15,6 +15,9 @@ }, { "path": "../../packages/api" + }, + { + "path": "../../packages/gating" } ], "compilerOptions": { diff --git a/apps/web/.eslintrc.js b/apps/web/.eslintrc.js index 331eff80908..3f9abcfcb2b 100644 --- a/apps/web/.eslintrc.js +++ b/apps/web/.eslintrc.js @@ -21,6 +21,32 @@ module.exports = { }, overrides: [ + { + // Portfolio pages must not use useAccount directly. Use usePortfolioAddress (or a domain-specific hook) instead. + files: ['src/pages/Portfolio/*.{ts,tsx}', 'src/pages/Portfolio/**/*.{ts,tsx}'], + rules: { + 'no-restricted-imports': [ + 'error', + { + paths: [ + { + name: 'hooks/useAccount', + message: + "Do not import 'useAccount' in portfolio pages. Use 'pages/Portfolio/hooks/usePortfolioAddress' (or a domain-specific hook) instead.", + }, + ], + }, + ], + 'no-restricted-syntax': [ + 'error', + { + selector: 'CallExpression[callee.name="useAccount"]', + message: + "Do not call 'useAccount' in portfolio pages. Use 'pages/Portfolio/hooks/usePortfolioAddress' (or a domain-specific hook) instead.", + }, + ], + }, + }, { files: [ 'src/index.tsx', diff --git a/apps/web/package.json b/apps/web/package.json index 6cd0ce8266c..b06f96e8d51 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -187,8 +187,8 @@ "@types/react-scroll-sync": "0.9.0", "@uniswap/analytics": "1.7.2", "@uniswap/analytics-events": "2.43.0", + "@uniswap/client-data-api": "0.0.18", "@uniswap/client-explore": "0.0.17", - "@uniswap/client-pools": "0.0.17", "@uniswap/merkle-distributor": "1.0.1", "@uniswap/permit2-sdk": "1.3.0", "@uniswap/router-sdk": "2.0.2", @@ -203,6 +203,7 @@ "@uniswap/v3-sdk": "3.25.2", "@uniswap/v4-sdk": "1.21.2", "@universe/api": "workspace:^", + "@universe/gating": "workspace:^", "@visx/group": "2.17.0", "@visx/responsive": "3.12.0", "@visx/shape": "2.18.0", diff --git a/apps/web/project.json b/apps/web/project.json index 9d2524a729b..a49be623dc7 100644 --- a/apps/web/project.json +++ b/apps/web/project.json @@ -19,7 +19,7 @@ "command": "nx playwright:test web", "cwd": "{projectRoot}" }, - "dependsOn": ["anvil:base", "preview", "wait-for-webserver"] + "dependsOn": ["anvil:mainnet", "preview", "wait-for-webserver"] }, "e2e:anvil": { "executor": "nx:run-commands", @@ -27,7 +27,7 @@ "command": "nx playwright:test:anvil web", "cwd": "{projectRoot}" }, - "dependsOn": ["anvil:base", "preview", "wait-for-webserver"] + "dependsOn": ["anvil:mainnet", "preview", "wait-for-webserver"] }, "e2e:no-anvil": { "executor": "nx:run-commands", diff --git a/apps/web/public/images/portfolio_page_promo/dark.png b/apps/web/public/images/portfolio_page_promo/dark.png new file mode 100644 index 0000000000000000000000000000000000000000..910c183e24deb9b43730d54843d9036769a12733 GIT binary patch literal 232460 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%0APxrg1&5$NyZKw4;**he-L2uztDq8}u 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 literal 0 HcmV?d00001 diff --git a/apps/web/public/images/portfolio_page_promo/light.png b/apps/web/public/images/portfolio_page_promo/light.png new file mode 100644 index 0000000000000000000000000000000000000000..0387c71c436244e8e3b8aa406dc0345e9dd3c1c4 GIT binary patch literal 279063 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

H0!@vNK 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�smaIsoI)3 z6bk@MaDgp#aeX=Su|vMi=UE{rE!BY`tfLPrV}5{InFW5T`$u*l1msec3r0Gmuip5o zH#>CBw(s4M=JmIHYv`M&Ro0W)U;skurnV~5lT0kklSin2D=M`H8qKNsw=KjM^9=g!)hxPFp)lB1i@w6rXJ+V3c>O%hn z);X10(0G0w88)%X_w6eYW`3u*--@C>#3UiIoUb_zfg`o|pB%Q{L#LtA5lM<=6ef2K zUgjbC(_k zNqz@r(QZ)y2v(3ZqVmwjy-c5_0QWzwa{%^GPp2XsQ-lS7l?JJOYwbc98GpMT5 zA9op?mz1uj6}!C;cvl{AY<<}EA9(n3<&AjO8;eone1h^`pU|+f;w1DhK9r?PFf??m zAtA{!l-4_zr7u;a+E7p|c}jmQ0U|;254(MC6-I=&Bj)e{8J}FQoE}-=vbPqeR5~g2=#* zi3cNqU@5zfvQ*`iw+OP2{N>m^SDh_)+;dduK=fERLWEj~zNnAOAIsR?r07xWdu?7$ zl?^9Gj9Muz{+6cS%c2)SfRS>tU9o9bkX~%88r);nidGbLy+0}<qWU?ZR|y)G#$xbTIY zeIt~p!4OK|tLOXvn#z_@xe#B-;ch}TYW?)Hoq;MzofberJZMnc1yZG&q=U>b9#N)} zUw*a;o&-*cb$vZOus{O{trIg`u@P5*tJ8lrqnTK3PR$|%YsJS(2ob0L9%Z$jXe9Vf z7YXDy`F{Ae9YR=6s`h3EqrZ{}t-6N*f{w<0cvhPxD17-9K3@#5B$*vo5yDSJSZZ;= zxJamncEA>MTKx=*Lr%cFx-i7DDFj8bZd+5vfT8ayL2%SfH#kgHU_&pUy;aF8#r+JR2GDXrs{Fqr6Gch3^M4ANJ3kh{AzgJhz5D= z_6!eXgIF>^h-9mw9VozFBA>uWIIyo&(Nx5PGf)(=Wg=W_$Qg?#I(2b|fy%0GQDQCz zv2B|W)*MdM>!XWf#*##OG9sRk<+Xgi9%~(F=z6K!qF-u)*TbD8RHH$G`rI*v1D1Aq zK#WZp)9D2>?#|y=JT0?VBJeuUMe%13$eXuJ+|DtyApXUE%9Pk|bOlR1)Hnv2S5TVp zXxVh`4SdjqC7sW{)2lDfcur*jQC~pK5YWk+Wxt8{Nf{gx4D&N=$QX4gGF=)O?)>omx06pGbZdiaU-AL^}*OBgico zA_j~Q&WRH(%eX+%6x^o|^5TXRO7fz5S8nG$>QNEXr?Z1O6%rQ;i8|#u4YisWt4K1n znVS&8Ta>o1}{?)%cy8B5Wl8Y!k zNMupf&s8ju$O0^vE-1i*=5|VLk=8cOl4#fnnzxuGU4{-jP=()0tHAd4F+-Mqy>qj} zDkh!k>H1xgV034fiHiUd0RaK~v?Hq#05JZ#76)5lb$<}o?)!mTgMOeIUbL;fP?gkp zSk|6sqxkf0zGc3|U6gHsFcM9xBo|#Ol6%ZyLD^d3i;9GkLNiVC6N62!d{< z@E}Lcv8W0=YdWE<(Fz>6K(R19GSVKvC%rrl(u?&UW?J1;ibrp8DL^P`pSS4k3Z5Uo zn+YiiZjl{fn?*$f=*VBqQAaAh9 ziPc?mJj-^^$mTJ-8R%;(7UEiMSq{O~;pAw@(IewpxCcZ?(2mqvWn-LJ1p7Y&Wv5T2 z+aZ>r^{?bzzM2}PvI``wNpTiM*urMHpA%f#}#!v{upc$XyBw zCYZ~M7b0sHlu|xuzd6;TLc^$e{O!hXFDyh>h3pgWf zK0Pj+wT|&7C24rkGL{rf6J_nK`;t&^hcvBXkIDbwS%o2%6vSTf?{n&l0p>ir|#^WoqFG-o@xHm;o+Qj0I+ zPF%uy7H?0Hc%XT3gI06OWD}|iX-TU4DZ{DWI6LN@T6lE z{7sSqC8lmH5Km(R0FUJ7+wmp4k}6>ic@0U-=-2e~TZwI&4r@J8@?R!RI9F~vk^1Qz zkN8la6l`hYt#g!u@!JRmvIvm5CyRHd__vu(f7}u*>E=C~!vE5?-?9c;x%U6n-wr{U zd9Of6KNy6G5FH$aj~vZZP}cZtZp%40OipBnaL_svVQ@^pPUao^_1iN&pyf$$nWkcD zg;eZ6cL?niOYp`fpCmPGj^Qd$x1`PWY%_3JZSYnn4s7Z(+uI7!SHRd>YjkDak4Oa2 z*_3fXCt?k&=eV+bV(A(y2TT}K!ei${1UbM=IJVCr^+U*Fl?yU}CxVLl%q&4DGe=S7 zt)khNn6y`E9AEzyOA=^Vd^M(_K!O@~1?H!gh6+Vtonrf+pa4}_rhk1AM}i9f<|em7 zXgV`)NhwTu=K%ZFwLPb93QmIvj2EW6C!?ud#A`{!cHPq<9uH)_}cYY)e^u8=zJh9GXvNreztupPRQp(zG$*X-AKkaFyx1ysM+{94N=7o34y zMT?azBjot1G3kg|s8g_izJ7)S*!&EkBZCKtE*e4o0<9zFd_$LHq!)(E1OGb5OJ}^L z53pAE2R+@>GBpzE;}m#NGSq`v4BazTP_XEU(HB)cqiOU2en0_*9-p}EAk+Bv$uXLe zsh{_|!Ucrku<{fOw!UAu#Vf`Z*Ne5?k`qDbK=BR@e2$-R9~e_?e~OmH$~pe*{qY+3 zeF6PljOE00KJtgaYC4N{vV7QA=4`g;xl+t$3Dg3 z(}O<;;7hb^I-B3P9gXshRIab`wpAt!GEFIPn z?|)%|D3?-sp;PQ*C!?bPX$?VZZoXGi65IUQHXYpg2o@XT5r|L^ z;%R*YGI)JHcd-Qu%aHgEL~pt)@c=y$k-+sWd?C(fsXwA*DYayVSsiy6_e){-z>?`( zLwwu$9ys@#>ysHduCT?ccbTqvl?X0=r057)zyXB4viyWj5Xnd|KEhd*;d<|r686w)0cX$^1! zb2eOu(g1GGaJe6@TlpJe5hRH6p=9VIM4F~7WyF-L^{YY}&)9cFcf~0Dd#%U|e^2h{ z)uj@(Z+*pC6ODUR5<6cO2?-S<6M*pP*xqJ$X8R+kr~JtWhzZmfm)qpE)n=^I zDSdm20;;U((k4`3$^YsZAG{@liD6_jo0bd8TGDSImZFchjV)OkSBX`xe$^=Ri|l(3 zmwHsBI2zDP{>r?%Y%_d`*xs>Of6afMMa+BZFKNa%QoS`4eb8&N6X$~pSBmGHt-lUR z9^ji!iSW&|)#}+pr^`xHX1{76*P9eW4*|&ur(cjg#=kA7e)amhcI!JA9GX{84qh zp_mG6*ya*6FvMs#cQb-yqvmpxw-Fo1bJ9JbxwfA|5Way5hgY`ihzG!i1C0@{8BUr3 z10ngF(@&5`zIK#0y?91C7C()ki|Insh#JLL8()F0=!0|%B`1VZ?l%Zk zzgY}F?$i)}Ya{4@b>IZyl+}I6fWlP-q!{Oq@_H1#BachI2KOV zO7)oy>GtgXi%J_NsF?{wmi6T!h7!o)3({e{*~G8<1_R8{I@DdM8#cPIJE&{1eyf?; z(EyAqRA?NhaIk(m@J{f1YP%6#hmUL;M|Op_so(yS`+j1umV294WIG%!MLDl;g_0eE4(+}Y*!g3XG)*p z=!tlka4$D)YjxO=JV(gOQ78sr#m_LqgWG;R29&%=atW%7+VY%qMucsif3+U*osuZJ z*{03bmw{3GLtJ6jKvjef?f$05|W!xL_8ra|ew5a?Pkt-UT z4K74S%XGWq)KrLO4z&p_{My=8jLrf6O-fwu`)0V56&r6=oFts$TRcu5!vpNW6oEjo zPVOQzzW_u+toQOEMF1Hn9CVVe?R}>=vp8~T$T={b2D&GSmX$PrAa8z5#0f9?W7#&0 zrC%wAT6~z@hdt%Qoe(IVr^2@*zJxav`xN*uJvMkQYx7N%^EZDDmLCJA*&~C*6Z2l_ z+H?6IySpeBI^<;)!0M*Sa#Ul?Q9-FX269t0npDP41kqYQ;hu?>&lP zkkdWxM*bs_A*Ub!-3OqrHoaXdaf$#Muz#xP^)-D!;N=S%==CcD9Dm=S$e)_F;@3oY ziNAk_(&PDM%OxU1eedac;1NseCx8K$0?6?MVpxdLilabR^Tr!jUMx;mQVF$txUmSw z-4^oyB3Rn|gesQrggtb1%DDtP%f-Da;5!VM$)xyft3`IUd8)Fvf1yzFVm`a9{9qPj zWt_k#IOnP1*>zb}|K||v^L`=kK1o=J>&tP(4o}&wL%hkoUwgfTGVmay4K7@!u(LEg z#Z-}Cz4P60#98JcI+B?Z=A7)|Q|=abWvf8ug%h;dGG=#^QzoX@5$d)fd+Hhc;N*AJ z6z@&=AV2SGJ+njv>eEGnEs8yyXCfm%hitmxExL62?ody}h>nf?+12)Tu-$4H9NGT# zVlbZ3FH!P4p>&Gw;z>{l^g@8!uWND-+Yk~*^?!jWP{Yr^9^BbmbvLtwlF!e56<0@2 z{N?>_D3Y>&D7eGU-af@^#aWul75`@G0gev!-%DORtv`g*Qir;^YA40?T{E9?)JRxv zs1qtpd2A?ki}SpYyKeNpEa+$^cnS=SSAT46>1(vfa=}#VV7tjxfYj@PM!o64b-!D( zDXiwt;(#-ZEz92}tL)_(MfI%{Kb*ywu0p5FA7Vaz5~Wt{dXD}9f|_qoF2UXKHaMT- zQ`{f@6S&DHVIn$Q0%5Yz`QtqZYqoSzYq#)PhVjXki4dt=St+tk3)Z@yg6oH7@*+_E zZwF)=*9Zevy6nrWf%B)AyysoB;U7~JI)T~Q!%r+}31ufn#F=Bap_X@y0<|*) zLpB?vFYbRv%yJQe5ZcqNPhP`reo5vJ2+hfPRt*Eaeupibl9SdvNN&(W)=qJOLAdvQ z`kOq>rd^$NI>neYUk>;asjkG(yhFh`3as_Nr+Sn;87O#_I`fT}FyTj$R?tsDgZYK& zSin5oV>CqoBDa&s$MCHlGQ-IbSu5Pd;x?Ly4;9mA7@4t(B6vpjB&F+fzd7E~m`uR# z=w49pC7fn_eml)OzN4LGh&~#oJiD+C`rldrSm3tH(_E_5)f!S0<4OLPH}+#`k%4!g z+x_8bm`F4uDl`p}+`^J{3A5Vx(4;`yzPny-V&xQXtboyYD1g(?HmEK-~G^M(w9-GmxR~AEC)?_u#M`_&@p?yP5%CF{!u}UBA4~J#Sc>fk{Uo%Lj(lw zs*DQhx()^i%As3mB0EM2QuK)|JqDcWf4TSR*Lff##1alr3Ir|f=r{4`?!1EjMuDEx zu(M1fTXhP1RO}FHvLhjUXdls{YvHvW&X6QPF@s`jHZl{2Se7AyrT+QmM&ZbE%pL+1 z!Uq0DmhbNDVz6J{ z(0SIA3qnLl47JUd&S!cxhMRbzQujVU4V{Gdh% zws1MVWdrJpu;uPgaYjl7h3A%ch+_bywnU+D1qbiMEW%y3MKop|A36M*NPkP~H!Mr) zO-tu3p`DWE3W)L9*KjaL^l^CUIML^u6C7X&O}n?ia|7RaLBq%TthCx(9N>u}yiGrb zAM5}^i?&L#qn1&Ph6v~O%wAAgmp313YSTC%r4)+h*1k3KcnHD!AIBPNgKAoG720d- zuCR^|B7ZBfbi*-q%V_@wnr6hhXP~8Vx%1GOfjwyDM*NeO1t`%(sGkIcLDOSf-QK1sMGb9$@WOh`adgEAIPor9m z3J+QKwfuNc3B=66jx(F+I0!xLGkR;O`{ZgtL&3GYc9BS=mhsk~eTI>T6Z-Ye1g;6{M29z!(k?ziwLQ3yt)N;{!U%&@ z>8qPKOR^z9fiSGMp*qd)a~7a=YLw+K{#AX}+#i@j|2?HR<|4!(PO8qfhQxv8Z!Jd* zzj|;^*xIfVNxjNtK1zYdraK|aZlZatkUg7W&(N|t0)$}9rsED@*-tzdM-kw8(zd|D zl)gv3%!i0*eCndb!~X`85xVo#S2PSRFG_7hUy{uC3%&YVY(!5<(o$bL{LKY?o6dT< z7>J1A>etlDmAJ$mVIimt%e}T?r|YqGYoEdfX5>l0Aw}G2+i)WjzZ*Lj7FU7-0QmH^ z`i!(V1JagW`rF3R2bjPB^XR+ZhzPp;0t1O&57U36gt{5kQ{|#R_8ibqGr2W$t4_Fa zR6qts;@ZOL7BLV4B{gWu<&qZ)J`oJmejhk)t;Df9rv7U6X)pC&H+I8^N=wPQ+_!p7 z6BWV8%^|VSw{5ps**JNV9TlNQ0i9<_P}~bMsNVt2zuS!l;jF0ARVlGB<WBEdgS@xF=i zqKC@^z?tF^e&YHj$xZS*mDt6YE0O*C`We+QGVQ zr?7T|h=E7i{=%pFsc#!uFhgu|+_I{|V}9FXaK$UAy1eWE!jC%<&Vi{&_?;iAcbDh? zUBH72uD^95yMv6p*0yMErIEV0DvlBC!Z~_z>Djz!JT`D9NiJ8izi+3-&kVS6U<**tDwgk7-kba4U^Ot{)g4{$_IFEmAq@EFs&+K*JpsLDr`MgtM zijTnd*xFTdgR1}q-uIwvvV)T=QTCq4X+1+M7ny>$YNHrL+bHU7Pg~t|sB5|?8gxh( zjwd>Wa`>AM3bwEICRGu*PYK-$4U{gTGY$2LX0j`vAIUnoF7rA236djXX;cR?u9A*j zJWCFb_XN-&O$-Dh;=l)s3^7b#H$*N?W@xS5{zqY_8yCN~_t|{`?4QBivGu2<8C)o^ zpu~(d^zM(SiKqg zT^l4Sb%n}5;hqr^t9UNA*Eu%5-71T=$7$hqC+4IPz4FTD4!wlF`i49cm zVtoQP`G_eTdIHpgP=XoxHW}b7Wy+~71LuogOl_6vB#-6W*<491x z88}Ua)&VOK0n0wn2o7(5F4YP=vFWj8%X=C z?AKoT+}0jZ=m#?1{dLY9vNgqN%w;n?KalVS`K%rpTrV{E_29%DP}YpO??ATFgEyXX z5ZAM>Bd6DWOF|Pc0Z!pSAgSx|`8HTn#9$1FUQNv(B1Hl(D0{^-W!0*g4-_q19r7+Y zVFt_V6dS|QLjkfi$T0EJvcSyMNK4u&sOO94_{mmFdQ@z_1wFyqvu#Fs}E zb}Qwd?0tdHL3m7GWV)&by_zm`yak_UpENNS^NyxE!K1w#vy*VSJ0Juh)@r#tkqMH_ zh6L_-?f?lqh0PubQw{7f8&8kSbN3xZt_lja))69jY`9)bug}&CYUCyUX3n^+L4%Gk z#l$z&MQ#$MW5S41odR= z{@H4}B&^9jPMS-ACg-I$T#G`qu+uaJ-fIBiOuj6J2%4VvE0u7Gz~c&Wh*AC<`yQEz zR3eOnkiz4)t%q`InZpT6IVK*hs#%Btk34eP=ogelTACADYj|(X>Mr<^gE9wn`83>f zae>D~Tdh8^Xq1w+hWsub<&sdjb&dvHpfRs)q{2|)Y_`wg>T7Kqlf@aoh5~2jA#cV2 zoQ(Yj{}~K~Ut=`@jYm5Q1;&Q*ndaUj+rfZqJXot_ogGcS3i%@#FpmpIxdeJ6d@HtH zZ&%~v_5uhm(c5-mt8sUDMCX%=_K^p2(diNY@Ki(`KG$ zy>5Q(oCwNhoVCUiCa28)BqTQ;NJO`hup{b-<>0Hy(*iZ-PfG2j7+nC?Z!XLZ7Z*!0v z(l_$s#m=WzGhN{|G93H=eShegbNMdeoif@QrI4~tLxRS@)9g-_f9>_Vc{VsSVYpuT zy&CuEAz!A)_;at5#GIdH(h|)k;QCchoP{wDJdxJGO(Pm1t_$dJoKPlzZaTUC_Ql6+nCg-*M_w$tA9K2K(-lfS8A;Gr1VP=O(b&_(E5^M3-VuFHaJ$+) zsJS`!R;oXXOjnhs2x!(M+D?0EOCwiz^QX`)+zG1pu3mIv1(Wpy`X@2$xW?C05lPs3 z^*`=BnYzlFR&W6r`FLWkk1q$DZGnPKlZMmU@ceVl>`gub_&oJUAU_Vs+Q=Abda_hs zbRdx%>8oS9hk$0mx?6ipX-qLum@G@4R+z`_FnVJ@q5Y?onktSvM{Eok*I0B#&9EKw8YNoWd9m++#NK)pd}I z;22taulN^vf4>ic2VKb#ftd1NwEdycA20xhg{^#D1hlBnG}7FZcRUXjN`iRlfu2FC zVxMRpF)%6%vPWSe|Dd!#AEdG%?1|sz?$Avs8xNJCKlM1rD?4<6u|m;jY?2qB6t&8) zBv!CuKl7~2RWxGzL4rS*Z6%y~S}ir%bmcEC7%&qNH12YbZMS@u5Z%6a75{N>Lx>40 z4gC0#c)g)@YC_!x|De!T@8t%ki`$M_nzz)(R40zqVp|>3$ULhlXJ4YGaS_K_5>0Y?x>S$C_k`x|Gk+h6(>kG-q#q7hJDw8dngCDVXi4y*TTj(e`-P zbxnHf+helDDbwbrcsLVm@^vd9tlB}*VrbA;e#WO1D?)@y6O;326gT=q{1IIHkN zlDANytZl;#Wr#}&PX2%p>}~G57#BF`0r`$MTQVFcW?17>y+#fzgTyA4?{8^LUG8ng zaE3?vq|+wLZ4hvNq9a$nGw&})TVCj=$SN9#CRuTZGP&F(4T&&2?LY>(BGxXn6+v5K zS2ybZYPuv3GEx5x@_Ufa8(KK4seCld>p1N}I1%B@Z2$UV?jb!u3B=^m(iNy1iFtk2 z)*IgEFTxZ!?tAEW_`Xw_xO?N}1U#7w`9TF`71Y);&S4Dib4qY1Ku`|wFys3-VB|u^ zp>aB8Vb_z}5s6{;lk44~%(8*XUCwCv(mnPW?~TKyH);mAtxMIpqG3J464tnhibyU= z_ynrQg_GW5Hi-st&#n%!N<1rK(We;E%XR&|nMTT|t1)4H&a8xPcl%oLZ|x(I;kPaYSlg0rJoZ|^IMXZN{6C6x77IysuyD+z=IsodrB2^(b5gLd;3Z)WPxkDm)W#DT>l z=2^PS)sQ(GiguEJ_HZ^lnF;$=1FoUx&iWJ5^lGedB}aMlR)5uxjch>uO=d?*2@LV3 zAbvU+K^^y<85W=TDaGJ-4xr@^+#ItuTnwj2acoWv0tJw~7{Tn>f3pU@ED1hL{*PA< zSC;-UT>46@0P|Wups^lm-Ms^JqWSIeb?_I%u=w$7u+3%H=kN}<7&@;nAJArx!Q=6b z#~g--0v!7TxtAPt`0yXY#}1bes+9ii*(n5q@VyE+4Vn9q5)0{*Ch+nI(xWSLGF~<% zh02CmKAVPF2HeyqL0|tT#`p=3E;@NDnFDU)o!Pu+cg0Nq=8mH9Z#o`)D67z;k2G(a z=kur8lM2yM{uY;sl~fKLxyD9b@tXy7IRm)TRSlIf9(?N{SVA5&V}V(MnY8WH#YZ28 zBKr1FTK9z3ULYL!8;7Sh`0I(&62%!$nCuPF7I>OFBogpq5znVGbXAyFR;OKD0V#|7 zC<8ZmqZ+hT|8FZJnBhBmlcw@v-QIgx8Auj1#|GC;V*~QSdWc)&=z&tSP?MU2r_ta_ zi8_L*CdhkQjEAzU8({Z$(;IIGjjwj&)#W)HhS~D&+t=G!3E{HL>k1>w+j4)1{|S={ z8iDGpx;CSp1ztkyx)8!VtXAT6K#%;n^ zEgoZ~LEuYiHj(YZmRd~wB6_s#XQoKU2{ldv0%t9&fRbHF$%V^5y>KKgLU1qKRl}eb zI$*1(zv~QhiG7o9){Jmln+xb6A)CQTh$66vB={*GB_46y3;Ye}>v{VMT2 zNLV-6;VFpMS0J5MU*?~2A069Di(|o_&?`vVp@vj^_qMLl2}fwDNJ z7MrE_T@X7!6*aRv-AB*@I|%_#A}Lw>`-IoI0QmMLT<%C@*{iD3^7cka8q|8mE~;~u zEI+Y_THS!|vacN0*NP_ejjm$rmlxGl1TGPQIfgro#!CN3hgzK&m=JrI>*FC!8CWc% zuq&7F{Iz(ZlpU$!Tp<>;oy5P7kbthjWqxw2-$3(M#mU%yCY+}RuCmo}_Oz^bI9P^B z%s5DwII<)WK7sVJUVZRPYCdPIi0n~qkx)8+l*IEmuOedL&^7Q3?WPtoD6p`DQ^b_h z5XM=d?FT-Fedg|6B884hvUWCH??!%7E$Py;^d1@NOY zotdK}p?!CHR74KMQT0Hhcgsg+MNZW#q0BjI2bOWd5dy)I!nbnKU^^qkg14Tkn_xkK zUKW}YfgL4F?cTT#uUd|jC9pfCY7I^*=zFh)9fCA|^?57%#r&_6KKwX}1aYp0UVgz`?zDPwW3?X{R>K63 z{}|)$`tly6Bd-C{`{cbTrsxqWp#XX|@|l{T62-XJ5yw?Ksyr+PklTord7@@e)l{d&B67#>3h*S->^=x)(#j@rxNoYm~QWmA;+8){^+>_;a% z-Bynn40#mS3&(AepjtM@ch=8pt(*yn>P#Fo7?IH*EtFj_pmRE?m`HIOojpmOUGLt* z2>eGzRp=Z2ajwAmSh#Jo$k51+Bh|_@@YC|zfW!$YR9og(Y!2D*owh`0*Ma7>YOq1& z&iEl&^m{jaFEQg^ik%GdNHp=~e?Lg1UMgVT2L}Z_Kb|g16y>mAH9gxoKSkW?bFuZ- zCRN51zvwjc8uX+)4l+ovWm=wZo1o2QYOBHTA52uDL`1*8LT^dw&D-hxuZ&2c^Lh7h zmdhIxrp=}-N5$kWwihNFP(T>^<&@K)XD;(ithn{s_=ghQ)P$dzuQp!RKW$y^2CaxX zeUCHgn_qP9kBb03q!+rx&eo2PQYMc*rgeODQq{uf;YI{|5%3y@xGEuT^BA1>KEJGb zXZw%2;@?8|P9C!BQ{G_VHG0taw~8bozVnr+8Q=LAXY2)?k!IeqS&N;wtSD&Wb(lSj zFhx3Wd$_KP0gotMqEvF)9?JJARvXF9eilkyvw(SK5x-=v7~CwofMhtj*eJD}49%S#hz+glRK%98EAd4^|d zX=l%+LiM&^`a~^-qlXd59XFzs@yo7p-pv>*J-EVk=Mev8AG=@aplOj{hV_*Im>~R> zhpH&~lOZ#(&ROS=BM^@u!_d9;`{Nd;M@^S=EnH{xQGf2Rz{+=ACvB+EL#Pxu-26x> z96wRgCuq*XQdHi=m^6?7k8;KJRZV58Dgy@J;w?5uEwNmIU)O-kx>zKcQ9fbPKX|leo7h)%i?0zrAbvPgx~3cC-@& zG2pRfccUqPA*|}e?2mHTnCm@YOS;uatNR+P)zHM1Z#R8{35%#`G)PJNmnE%9uS6$^ z24>?!V_rvhtWPg#kRR2})GW7XkTWyu&hO+z6%Qg{48N*LfVTocvgJYC3YcYG1t|bHq<$|GI_#CKBO;6dO}$iR3{125*BsYY~p5YvLByXe&>t^x88$W_Q5x` z%DB;&`f#vgZjEDB10T8vz8gHTp$A@2=y`Bu)(i_9NQD(PgO53K+2Mx4E6B9Ik3UQq zf&2-}4(|!}3vWJUPn|#z398+f3=NgY;AYY1g7S=--i|jJM=S0zNQ3e}lhW&x(t#<` zhDY@ZNS#on^JN&qS4q!3+#V6t;ABdbp&jD!K6%R`I782^MLN~HQOY#r`CmD?%f^aO znci&&$-OD+<~t{!VzDZ1zF*aS4^W+~zbao%S28qj?(XhR@mW6K`?>#xo1d=T7uU?3b7tqvoHO$t#7=N1fdiq2xGHk( z3%5*IAm`!LpWjN2K{y=;3DI>YA!QJRM3D6G%Q|7H%&3WV*o`+hro)i%JCne_$XD%O zf9kV9jQMezo_!fC9i3w-f{aU)W1~WZG}QKCfzUQA?Dfi$%sL0hFg^=L8&#+yF$K2k zR^|`ngd};T{{^|a&r=ZL^Kf%_DID_=C-{qes0PcFFDrz znvnK+$Kt<{E{D{WWK@7ZPoolcZ}*sdv6G!d-z6dfm?|V8NQc1)DNDh)wvxvH-%5aq zT+l%fcU!=h#f8Xy%O-B`iAadY3b#a-kEum{6hA%1CLCI^ljiZJ_p?mWw;I904s7i8 zEaaUMewS2X_82}Q6z|3q$xFiq*`&FrH{&d-NHaova7#Ni_uF?2$HZHMeIL`~s|BGj zcRq`H2d{|itKk=jwvldGWn(PtKs0~ryoYA|#Sl|Bbt;I`)|&`vB}_*8pTwc?_Y0(r zjNeVA^wg(*#>J;F_1V#e4mzI6zdcVRVhUqaeE^aB^Bkd2nxXS$pL ztnSMcog)&!_%@NV;Bd_la}szG)0EG6=oVcXAI*f9vxM;tnLO;D85mr1tNF6mVt<8( z1juR0pGZ!(jf0GT?7|unt0c-*Mv0n~_jfN0Aqz<1o)IU{h(y6lJFZL_5Ussw4h150 zcS))-E;>u39mq@7PdpDk9jd(rV?}o=PGWSB!ZKY9Cw~Rr zk`kKxp>BjoPOCsWMTRZ4{)3B)f79;Wj2*6JDT5!+7^*7-s0&!ry!_0A%HuLwn<#z^ zGW2#{JzJ*pi9z0>JKaZP!w8U^bTjKf5^KybBB1V-8=q5f*71EQ7KqHPJ{Y2sOx||P z*YRG&<>|y^`_sm9Sc!9A^Y8MurrY|IeBlzNfBUpuVF%8JyRQ;nSenVikh!lv3W6=O z_3kp7vWaJQzEvmtmTz_U5S)LS&ZWNC~R3*skU zC`85RWEz_=@J+o}m%dCX5*(6`@pZ~_cE&TyLABGZ$f2Pb+=83_B<^_alFii;nsJs? zR<9?JlwKWYQkqTZ&ulcV2LwC3FX#O)HMr^d;D-fa|Tixz)_%=s99O;_OYN)$!l*-&{stwsQuWST#BVYN=Bu ziGW2AOa{pCHXQ^xfq)x5aPMy6yK~AoxZbt*N#2p7@KzTTyjpn*`|a}OJ=A@Ki9Tad zHH`@IhTqLNgzg;bgf)P zJ2Y}A5vQiDF9}C;L)wKMzIW%iYQ;Cr98*PdrZP-rvem5DsXKh+hljJ%?aXAX8xbCow!J} z;rSFukg?7t?E~Ky(!X0UL8!H9zpaI0y=qr)q|KFZeA)5+w&M=V$)zj_2YRtJwQ|Pi zXv-ZDs?O)ZJ(j;{SW)FtP?jrL_13ZAq-(={74XHUbkL`QAZNp@h0J9?-vdey^QIJV z=%9O2aPMSz;6zU9nO18vlOC{8_qR>f^#eI8#1)=MC8GPQ<{A~)L22a&l@uHp#+8wt zwu#@s$qkf>9p%wAgRkQQYI%7T#t-NfVx@kEPi?LAcb^~49|>?EP^krx)K@+Rc3hM# zV9WxO(+0aCCt&5-wZS!Vp23=L&Ijue@Lz;-n})@@KGNa(E_TB@U=@Z*-=BMCRTe=~ zj}EA>h}T_E|Jb&sBl{tX9JG0zyg|A^04-X`x3u06)_G#D53khI!`A8t`!_jT#~4I? z%$nnkW?wXsd}q2mAJ;86G3cg%Zk}#tj^Q^Z;|dSl7i3LW+8x}zsLt4GgmaWLw~HUoQT$GR&*X=}`xKgbFJ_^coV0dO?F!$cY+9l#9=80_RygA-;p?x+&= zdm9n`kj(i&78n>m$>C7xJTdAJ5C3(kb>(TJ7E2FSfw6GCtdJ%~+U!YadYgvs$rnIqV!s`+Z6qC^_3()!|j z+o+QiP=h^hgI_M%Fno%=`kw$F0|ULns~t7U+}fSnY85Fh1Zz1mB)J5(7=om#$aVwy zIes!M7~E_MK#mC0uK8GSVKIrUsz|6|7hX3vw|^CGHBFIgIOM?KAoF3Uw9A4Oe^zgg$6Ox@y!#$e{~oDWk(P#>ja<&YA%7KDJy&x8`iM zIWqZ@_g;3ZZj}q)t>o4%Ae$U(b_(9(I5(r(v1#(i4e!W|vGY6Ohu7wu@c9k?F+?^i zer*KAp@R*tP>7z|;G7**hEu?%pW)Dp)1tuRQ=V(L0`vIv13oebuUKj@ z0_mm#R`LTNh2UvjcH!Bijq)MHnG#mQtw!Sgv^TPhF*eg~x5DEup>z-FwD1eOUOB<# z&L%8K6Ke&dx%VNeOp8~>%Y@3OljLDM$m1XSWYf#hW7Q9OHnhx)TPGG*G>0r~0)J=c9v zraOyCj~A)(s*sUxYNMfri|RQ~UAF7si3jMke}kPyf$sb&r#G*`Ecs81^KNhsZ3gFB zk9P_-&~C6nTb7vBG$v)@8gkk}|;C4RorT9I_e(wv#3I>k6XCYI)&BQ{0Xh zGQEHcna7es)<)hR&Uw$AA$X1?;u<&%j6Z(t7W1|=17Vxx%ITkV?Oy+N4;|J#e(PuU zB&r1lLL{w{$Fy#}n)L^vZER40gRY~4^WY*8f@dWzKq@DPBKBSDwE8*JQ{^49|4l?u zmP0)D$Mbt3xjCf5`ybO^1xW4KU?k|EwifkHDTUgF<7lPXu_ox9FHs5W)I3xu54L`P zQ}sF{RXACg-ln8!x!l4zKUyfD!(JM}t~5-^u=eWLVG8Bj|4y-vbBpH6+?7K;8VyQl zOr{f!-akJx@yL&9KZ>8@kyO#RwcwFs-1c35Kj|~mmM}u6KC3r5o06iEn99nKgV*5Y z%r}!BMM)QUBtMcQGr)}1oYrjq<`O#mb^)#z1 zd8OD-7R_Hvr87AC>6;U^3$q$6+NVe1w0hpb9PeU|rw; z5ztANn4?)UDe}obQFRS;y$DX4f*N=O8voH-xdhYZFr#JN5{z?UqFTd%tX{| z+#>ttRheNfJ>{5Z*{VTD#Nt8PaZ!7BI|Dk+)_@{593OnF%w}gVQ;aXFXQFv z6-w(Js>mBMXBsM>jO;Dbku`+JZC4#AuN*Kf;ooQtH>C8F77H>qcqvq4_+3{&vpPJn zoyMOi*E69PPJFm;pwA=S(zfdOfUa&T+TX`Pd z>{d-{xVf78WTk|oUCX5peQm$apaWxsH+q{tHEbVwd6Gdr>za4?x(SQ>Z5T*zzm**? z+HMC)N+{{HHn-bOnXoY@u?Xgb&oyMNu}^$|UwSB^Ce;6Koq1#xQ6UrkVvubgiUNCi z?o~iF$0S6q5flHx#x~wf24q>2Wt-qcv!3Le`!n2lv*F40c_5Fyuv?!C?8E96f$zM= z08W$fj`^I6rt6De{h6Y;!t`On+E%bq5OZBHldr$(GmEp-yWa>2=(x`AX2ixInASr> zoVD$K|E`7&kx5mbI~biD{q;?3O)({c@)R4^ad^*lD-Loq$v{U`pUFb0kBZ>dAj|PN zo6$CiZR~1$1Pu-BN#EG)(F9g8Y{z{d-x2Y~tg z4hx@Z)i#FEhCHW6KkCDj>0AUc=I;J#l1S-B~bHHft36;n-Cc7GOHF)ULha@osiIw<`Vv zO%e@=s9~z}dG@pzLVhHy>hcyG`-o2X_ay=;h(8$`aGtNm@U-O}62~2L#(q|7fARjP zj!IgYDaxN{bVSj=J7o~|qW$Q8om{yDu^+5y!3EDXk!3^V*5W;$XF{*eo%(b3 z%E(=H8(jej1fI!Gwx4LiwPt?ZOQ@^D5^~ofITg#JV{h=mH36yg|E-=6y5b$0MJpMp z^a{wnQZ}4+jp25AUgctC9W!uDmq0g8pnD?vq)vUtXHZ_NO$RbKqk`ZJ86iB z$7g&T59qL2o)CQjz1yIlOujsA%KN5W`tF?@3`6($x^Oc|q^(b#?{9(pER*M!k@gBM z&oL@Y_&MaC#j#%ELPyn`gRiavzkiMq5TA15{m6mzBX zIhbFRJ!BF(lxReO-0$c3*sbOytf9eoWQUfU(Lm`#$FH;5hN0$~bbIVHRR7_yi4$Ik zaA{@F-`YQuscsgv0QZva`wj}J&MjaTc*P_f4mrU(Uy{dt(7NrYv?YOdNP2(Zf(8s!GW6B{HexQc|ZbF+P^8TzyTL~2d7JaV77J1*99nkgm#g6`Vv8h zw4q%BT88LA!6227|9-s~Xg2DiVv#>52Ca6(ukOX#^zE7UBX!os6Zsb&_p)X&TCV=% z(-fpV%|6{mznvMV`df&4&4424qp;nu!BRy0Gy#w}8>7VM(1)R~zoR2+y2Vuc2XuqS;W3t2 z+q}PTRy`h6oNdb++t|%vMfo4QKsyzXI~`dzi+oRcBuJTf1~ZSoj5$Bc^Pn=TurwsB z#DMEjtx(69da~3Ve(uV3b4t@o@8=mP5U1)eA2f(}stjW|5tCZ+f}7XI-Sg7zV8dM}_hU1!QkUi^|4E?fnFVZ_&0pE>Cd(bp}tDrGSv z+s{CenKZHHd*K8?aHi$Gk<0r`)Lj8$ys;6_C{V=5mms|!f7^TIw)rj?^DDAFw0qze zBkWJgOWm7Mq{6!7(d3yMsWr(HunxDC`6PU?Gg7f~*2$mV2}LFPDq-7VS=45;OlNyh zoN#bKK2**&%qqi;vOp}W9JOn1uPVK>Y+EZJ`%_*@P~e|7zB;`7EeiN^9kNEMX7vH7 zW#(iNO*)%}&3lYPa?f6E24<@D_f2nRsFy5_zyTr}vY0P3X~wzHsmlDzEKvE|q41@otoYfX3#De;!|CU0VZ39Gp?WAK+MqJUmZNCfpzX?K zJP6s@=kn)mN2aqc7p9P|NpeOsu1ZTo{V=pWRnpExuk##!HBEw-`PWXEZ3SE8GjFCa3*7IGkQ5x)UoM>x<*hHM9V>$Y>6=ylOe7~XZv0+Qi-i!qCIT~$J5Bm*L-EB@jbGmF&@|Ihs z(IPWh`M$?;S9Le|p(^xYM&tvT8%5Rvww4!ZNILR&Z&52?J}^i%nq5H8)i8eaTWpLN z`=UvyQZ5H4|CN@k^y_ngN`BAKH3``|1Mgkt2-~+aLXobK`6T@`T(Z7(j})UN42!^S z(u-vNwM>(-?hB?w!y+3}5EAos#KLlE4L z0(3XS7XvaR7QVY67IQ?Vw+d+nbg&06e!VeDV2vT!9~nv_Wd0PP^Wy`lp(xsV z`NMywY6)c_yqXPIa+q7TboEs}SEv3zMU6kwG-THryV>yEwE3;dxz=yryrRPR`@`rx zM36N0T7e2HdmMj~?9%9*td1@Yh;u=5q&DnXNdaikKxA`JBeo7zRZR$((Sou63pit@&=ilnfE3=Gx zp4;q8eL=_%bq~8fShwNIZieY^Q{fhMq*bD+YjC2OTE(FoGQLh|+z`vkbCxsb(uT_0 z9dQVOj2sCdLkbM>wj&(y#!kL~$;2kxORyHWew~Rlx*Xy-@YNXQjT9$O=f6=KvU*@6E-lCCda@K9!tQ6H7yl*$8?Gfmb(OcQlg$mf;@ITSM_ONjGKK=)oT7u} z!<4dPiV)((Qc*{{Yy9$M7Pvg_om_`42J3nuodoaxE01p_Nrm%M&PTH{-hzoIkkq>r z6s08$sP|HzO-i~!hBCzyelVZ5Hk2vRj8F4UXUDn=A$A;p2yA! zPaRu6lIPZWb=}l2q-rBRk`X)1AY4t3vokJjV`P&M7UeY#UDogx(<#t1 z?S=A-i=X6$LNb~$JmtwU#(TL?H-!MOJOYav2NF9!Vklb#r*Nr1Xa{eKWs4A;=%xC~ zzvEmw@`Hp%tkqjPyauuk#zY{--p@!p!vQhZ714PxFfak%uEc;WqB z!(^G$+CM&>%f`)ne${FWRYYq=y{-sdd|+SV2)nbATsH#)=w`4uHS8)Pm(VFuFBc{l>|p!QkGkQ(t&Y+c-RgxA@%TVa!G1bdcV&ZbtqX}jV}hoTI@BSLion3 z!<5TiwDs7wgY0&gRB_q)oit4f(uefP7|>|YCb;(xm7jia#DtbNycRv_DbHZY{`O)& zv+q}Ay`NhUazp;{t@0Atta6QUU4B5a05t}h%5o#n{(RKw=T}YI7dAcV-6-0|j4nyhhZPK)aK@XOLbbR);>I7lZTsrtn zO;D0T%q96(P`Xe9_y3;@u%je5;2k9f3=ZGXc!sqvYoin9 zwLp-g8u;YPd`{;YY8$ot*Sm_&-Z6U?TJ>({Ezf)dhxnykP%&5ZFZwl!FC{=2?Hz!k zuVuLH`)3q*$E-tCzTpCDT`hB3`oJ8)bY2iqWK97-sI0gA&9B9rK`Yv(TAZG=V<(q+ z;QM&7EMnGG2QZrWa!%5~$TJw9Jk9UcddCo62s^GUyA90-O#_v^p-^g%dPfA*{R!<_ z92p4Ncr9#{aZtrjytBr>MI|L?GP&))`uDFRa(S3Dc%X2OyP?Tm!xqyRgfBU?dG5+; zc;a?_`cdTC^Kl5e>ciwaI@l>GETHu86-T`P(VEq$<71(_J{aQc${<5q3}}}ksIdSs z@>kM(#$4PFLq3gEeoyqkfpqs+wFA%P6fLZjS!C+N-YgG2hjQ1+cOa#@poNX4@&aJB z7>Ib$SB&t$zUI4ZLXK;=_gS83!lx%Sbc*b4;uPfbZoul>_#WHadlq5VWg6IUpt^TqS(jPvffCXzqsSByM1 zh?>=RV3~?V{bdgI2M^;_;bp$pP*CNcgw|(s0NUgolqVP={_HdK6^1oSQIW2YW``kA zoN_aF5c%iJ5>hC4{~3$VLh6s*9CXCjfG2EogZ!0C_dkJ{2moma1EB{bn7bxPI#1rg zr(-~9O)Ytx*sh0QM0tYF&89?2^4ub2S>j2(n9uMLAw_Jy9Ww>TD7^|ddo7us*^@`T zb+pVZoxcc*ft`0~S|B2muSEO#*#OucN^hG|mY^7yH!(C6ck?-P?%HC9H)a~`83bt< zJ+R}$5#2%QCRij${w&SPZu+q=_d{%mWdfu+es4`>2W1DnlN%)=`U=7lOmu_ykeBi- zD*A6cmQE@KY>4pl4RDfd!onfdD_jC0i~oiLO(U96A*&68__2kCv3r0Q4AclLNVAyz zkkGz9|G!u8?1G5-6$DZdBecgozT;v*B=B!L`@MxQ zf#UufuR{)ImU`AXO|wD&SAqzFU)so#QvbX4aE$1zTcy^A>|f4$-!3+NJj(=Qx1#KU z9RhSa4yOPxqnl4}lB9C%7*H=iWs@2*tRvs+lWg0+s_*n`a$4!NJO&{6RP&m+(f+=Z zqV1@kSE{+H)2pPM@=1B^LA*w}jb)-a!1H8|UCc0hzkN`wHx@pJ-?0DSm}`Khlg!Pj zqP-2kY`^#UjDng#evA(Nt(&DjnzJbL)uMBf>E2=e# zXr`L$r5Fk*;0GTVp<7sfsnJHbCh2`o70rpFhKE#iAAK(u_z8~gzX9ZuXFsP#M92PQ z;S4wRmm$ygQzq;LmaGXlfzBJ{aBqP1cMh$kIWpe(}DFWuR)F%AB+8^_U2}j z05EzoD3s}Y+?YoGC^`rUu|IMTT^~h7@@Pu@Ud5I$V?_mt5fj`~$G~SpFkaEt$y-OW zg?)VFtmT9@lAt3pvwt&dUQN5I5G}Gn$-ZLCpJ_ z@6Y&_%t5qqPvq{Du&S+s3$;g-O3J^=jRs}GD^BN^QtIPu78qR z;D^*IJp$}(Bz=u{Mkb@39^Uk`6jT}(+MKOt2#!YMPCYU`W-ZWKh;1T&mm@;GA8b4j z{O0iQD80iO|EQwZ^&v}NYFrycjNW`UbP_U!L{P)g=6z=-P2Fh_pB@M}=ee3@?8}S_ zz(c^*wz5QeQys+PUUZ~UqsLPIyG%871gFcF;UA~@iz_zocA+&n5ibF~UcX53#uliZ zSd{H)`Bts0C{v?a>*MI~C;2s91|*_nTsSbNCGjj5{Z@~aJz_oY?v@_69=&^qB1SxD zLz4F6UMjK+TUguG2U(Eq{MizC<$f!?cVjfW%Klq6+@A?)hj-E{b@y3!H?@uTQJ_(E z5L%+h=-K*5YO2(;>y%{u7f_KdtIpE$mdVB*qmN6V!b=vH_XYQwhXllfbl{zxJTU|% zPzzMIj|17zM*lif?F44rPfQ-mF zu76-t?`F7Hr>Rh?(w22qH-)7^DgpR%m-`g>LdY+w@HQ{=FJ6L{7*H>@+t3XuDbj#i zX9tBA)s27KSSAxYtKb>z5DrW#$>Y==rI*m;+K~z51CEk(z9YU3Y z5>G&X+{XY@CLD{K)<=O&Ny65C{0+}ht`#U!kDJlCxuM>6ZM1qVF?^Br0TYar3)!fv>qO3U*Qx7+*Ie?TkI$D4>TDxgmpIn+$+sX_h0S0! zOFQm6RirKvqB(TdYyTe4{wpaf(+H8}q_vEu>)J6H)r@XAIoqI?RpdJVEITg~s#E^n z{BlNc{4<(EY_x5IDG?5Bt`D=#4k3qOv73I_(=8ik|x=Rf8|4ru;@pQZ%54fbK8WwKrGt96|4uBxTB! zxL`;E_Hh}uXr?yn)1aF4tS>%CFeniH<2jdd^*EwcDU(mExSDot0S*+Fq|MD zgg#)=q?tT&X%jZ#B>wWe_3g9XS;ZKeoXRjE6}Q~4p49#UlOm6Ki96Bwinbz571J&& zMk?36bz!tcLYr|*0nWNKSpF*!fL*tucrJM7iiFRAGiog+v+Hw!9QzXdV&#k)RU=Zp z*;Yvs4Emvx4G)TrWxNcz4qKm>lu;qNZ5bEuvva2jkOGU3ghS%0TzzL=Bq~6${Jg)L z#b7P>NCiQF=alS80PID*!6F$t!iHm!>A0-L+l*_|AO&y0MPD+zsY{f<#1^`XDOYsMhzB#eti?_hHc&@tV zQ!!azWl%j*Lbu2yQy&7+;>u9^Ke}>mVQo)Yh|$iIvsx0p$9|H~Pb_?rEf@Za{h7>Z z43$5G1b+#2wGjZd^Ff;YW(*uipKWHLjRG5ad62Ashw{D4J4j3^iFaz~SDn$SqDVW- zVCqc(M6Pzw5Cy{gcDCkqxYVmFMY3;q=k)XDnXd`vc8P?s6WZ_cnomt$;&h_yxWqau}$cSIQ%|$Nd zo4^T}HK?x0OP_t20IWN}G%VKEC2!WLpRttr3M3P?Sa30q`F2!0bEuGaxxR9s{3L*hMu>ZWG9n!s8Y^mR+b^;_38 zn*|~dud?61fXWY0`@upwR5-BSWM>?^48)G&|CZEfjm%FY%8(FIQJ$fDJ+)AfpE?4h zu)xEl3A_JUj%yK>%LbUw7feL%O<7MD2qXO@;Wi3o1*=;AkZm~CZKs+->Da-|JeyYG z$9=lv(C#Tbn%B-gB_Xt>Ufn%@_z>fz`jLh)$)u&EqLV&zMamQo5Rt%_U3xKNev4O5 zI-Xvs;KgG-bzS8X|VloF zm1dvqt4DAy_N|Hhj#eywYM4YCG4?Yc#Spsz8YHEgl}B{$aR~!-^JCq+^Gw5zqk0$+ z8E!_M$2c+%zNs*cPXd6roQO_a1J?}X;zWb>hy6{(=41Yqy1Dj8?#YsG`X|S!#<69i zqL3-eE2O>S+y0wjPgezjfd3@R1BWMS9Ajo4(E1e%H#Oa!?wXavg@y~@JJhs`b?}E0 z+xam2A}H6m-_ic#n#l)bRn=O9e?fpK3xsW0N)u30}6p z8Vmv?Z-60}bZYz~+Nq!f;sh~Uk+l`pA2F%6B12>#leJg8O$1E55lP=5%f_u=;G|#n z{bubjI;>*+o(a+9s;& zcC*ueiIFE0rB>-KCZ$`Yc)bB|Q&p%u9fFz&c+yeg*0n*HFzE?}=9Xu7zVBvbGUdw2 zHQ6f~4toCa$X4M+g+$gv_Z>2g-KX(VPy(oQ=(cXw_Q7cut0LG+G>oaPLSLy=xl1V} z*%;Rq?_$t&zEXUbZJ$a`Cc-kplmSBg_y##pfA)+Y4$Hs(+7co3W)0aPp>Tf&bC>9x z;-6e91nRR^gWHdoG`~_Qt_^lQ+@B}@~DBkcF|)iP$DLrw49aQ=9!!v$SZZ zEfnJb=_K)#g-bjGwW}fV)VoIb^Izr2VitffW^>=vUp=V%>EWhsr^hFVFdd|5-v;Fm z@m&ouFy-LNo)F=}^*z-!m8@CN=G*vIw9Zv4DFfs+MJFkYs~~#nn5Mq?O%!oM=YO}&A*oQj^X!H7*#Qac`(h8aJ?r?nlc|ESI6I+HJ$^Xyx z&j=Cu=xgw8HHA!h-l1fcNI3yEpruFsw~jj)mN(%69<^-`*;1#wFrY0o>z~V~kUF6F zdz3WLqzE1ResK5A%lCEKoY}&x@OZaRFAX2Q&ysI@=2;?hJq$mH*v|F71r(X;9gmEhme^SFWIO&+H+6PKN-l_F zu9L7UhhzFi6(rK*DR4zEz6m-*Waea1NR&ZoA>9%ZCe8NfhLTVuQ~%+h#qGLmQrf@Y zBvh#`-yg`+%G&R_K07fjao(LG8TIBb5C2K8Td|LJWYNEC2-7LfsU~Z*e4S3!emKV3 zLWXSiDg@r`!gD&hyzhQb_0t=!?s+g& zrc<~Bx>C1IUT&Nu5S=k9(Db=QN9=g00hUo-S#I3KNUr&6^y%$_Ny^rzuW0))DN0pe`1Lks9)N3;JWe4IaPUM%*F zN^hDGaR2U`5vY`q9ow)ZO!t)L1Mp^xP}m@7H%cx_gL9;l0LO!+NNsq&qod-cA6FG` zch-OXmcCIV#|?W21X5HE?&veJZf){4c3gZxopTbOzD!J*kb(^6eFCPM}Kt{gGpS)h0Qf8^3&v z6a4%IrbOF-u zyfn$QJ%d7V%pZQGCn|;-RWg|0$UP;wiufe(4!?>&IHVy){48&IwOLu9v-nQjF5gi2Su(8wFN`mMv4pn4GK;KXS8Aj}hfO`z(N+rWo8@2}zWoktg zK3C0ksXU`T$)!c|koJ#;8&t`Li6n>%9+jVBASAyN`se4udv<`=&rFp_X1j3{btjjZ z=2Y@BlU^V^jyQj>Fz#08Z?}bG2AaMzO+_Z`$KZlTVLA;aS#Kkc*f#_Mvp=ck5TE_P zg`}$6)hq6gy1HSmYuH8W+g#UUW{?5NDu3uZO>)Zn`2X0KiG_4^Z>Rm-_F#vM=InYJ zhW?iiXed_kvg3|-;c(i9^KqE5cf*ndc=YgE^l+u#{3fzcmxD{XS;^JfOI&wMr<6nkjd>t?Q@Y9Pw|H z`CEqhTc79p*u<@?t7bl7i*L=@Y-fXW4E?J37d&0vI1E~gJJt8R5{E=@7+*Cm?T>zP zeRbA(cTq8Jo6^PPV*{dS^`HdDzKHK7xN)_-x;`{`0s8k*=N}^jYNyA{jhT;cF7J#F zuyyduQ_|z7pK0-rffam=yQf0KbT;)iIUvlTv{eQefRk zg@qk^y^@YS400zvHo)kVdS^`T*Fx}@h@n$$@*q&k13Wt=HMcfc5HL`$6yc-xX4Dm# zto-L0-sCLszVeFr)ofD%cfVtaqLvNCYVAA?^C56GGQFB0GIP8yvq5@fu+@HLNLR4e zV>vEh3UDGIp~~_L5BHI)`R~8`Z@)QmCE9$9hfJ*tgsM>IbLfKH8k`Si7T1N=^U0PLAm>;%*$mgTWWb|9~n?; z#C#diLWP{J562u8V~a596zMjma96@!#?HU=eo+m$@L%iiyCW8x-hJ-GvQKiW6@!L} z8AyP2G^9i1^tgnqt7<`2uNq-)#haEn9pqnI!e~mEMO*Y6Po&=tIPkq3jnuWKDB~HT z%2!UyejoewV8WBA!PiDfOM+Na z?5}l|7@PTm7UIZu;)f>%*o|g!^cGS92U6oMuMwDhpvzr z^U0NaV0vJ{?zR5M2F~ASt+V14RWhZ8K>kP1FC*Ea>P0SDFG8lLHUeHMRCHC311}}B zpipI>wyLI#TcXB+Wy}7_hbIZg?dZpvv?3FFAGi)eE@S$obyM_J8!9D?f71GvOp17_ z#`W5WJ+1Q}fki7IF}h$MGS4RxPK5*e5piYQhy_nN=`zLpX1z@qV}~ZuhB@s7AqA=# z@1qYRD;poEn_E168zrH+f*es}kQilEFC)a<|H@PY*eY}QS?A{RLJg&%mC+0HUw7|X z$u^bI88I97lb~jQ+Z#d>B3r*ihg)#VxB#_hDGd>`j_~^1I1w^(Y_dAow8Xvf9D>=>j!IXD{!O%*+fJd1C zdr9Z2p$Sd-!bpbnBiv5uPGT@gZC|~I)=NRn=W>5>((;AGJK*CW$q|+n@{aCs0}tt7 zh|>A6TdZ~+1z9g{Y#72+)>sJ zR0PkOZNCC6J-+pU>FxsylA@0W*##S~;eNN1g|&upW;;{zHi zWRFtH%|S8g<_PL-%FJ*9kBkL)x@+52NRYgQqJbAa8YG@7@LpK?jmZ)IbHJYznzKNg z)4>Rv`MLVu8vJ!mce>Q(p_aa$q)T{wS)b9`yxB_6??Zd-Vd~_iYbq;Df4|_{{pf4z z5?n~Gr`s#Xrw4o3zRhEF9x z1l@pm-#LEfS4RP83Cxwc_>ND71ej6983W+K|4-aJ%a-)k$DlrTYdjbgar7rUznZO$_9L%@o$Uq%m| zg)9UKvu2h$RZbd7l%0hvlH>3GoKdKi@AP7^4yvkk3iOEQ6hG_fqVed}z7lYo&|h+q zYt}|bR00ubKs9_CZN){h0$qUr;G-Z3^s$B!&cD6qasz!|)gn@eU)MI8pm!6&%>|Ymla(&x&vt3iu zNo1HI^@-MkF6^j_PQEi30IeGjpCHpADmflg?XjUZbIrg~YwR zc@tBqx(`n>!g&Yc!qSL!mEsJd8SeqVe#Xvdj!c%_35_}3HW_o?U$iUa&(~-F_?x~! z40t>-*Id%(sfP{t)X=SF0d<|m0A5ROfizujMkGDU*-4@04qxuS_zb-z0Ng~RV|c`u z-vMRtLwILowdc3aQ_SIl4@p-Zw(6TQ>jwF*EF@8r*;m3745%`^Q_}BWT@nFm$A;;m z^FFNDVL@~%{K#`+OawZLVP;G}SHk0^&w0rWJ|DY&YY<8Y0&3Rtm-{gar(~{g+G;|^ z9VzLcC7Utr=n9)I-IMC~Be}XB4ph)NEbsGsy?i5wElMZ_rj6ZL8_0=aGc$&hj~5=53r);W3k0p zd&!T;jp2mzGo)49niV2|%@ zm7$%b{3+EMvc80pqqDysGhoA^h6)VxyC|^^hg8+rrn~JMB)3FVOr1?)_}18e5-~@R zORNcbnq*XH;R5{avpjA&;NO44cQOj7d)%B*{qE7zILyichPEqw7a2>`Yp@t{`Kyv5 zTlbI1qKemw_R}AkAZloq2%O%N?J#{pK_wRLg7&sK!*u8oHssH-^-Y4h`?7k|btN1V zu%er9XkZ{x0w2hU**F~1U$X0$qZ*Z=B|z@I|HB1n`Y0VQFaGF)g?#l0mWSLC?7O@X z9};k6D!jD7bz2}9w#Sm9Xl1JRjm_-?}yx9_7jn)T{%i4!Q+vvnpLPhkQk(J7FK^6c8^N7cYMYJO`|49 zwvDzSrG#$-DVI}q9W^*}pC=~p7FP~Bn-!$Cm|Ds*j`P%|arL=NFCn`&AjfnZ`yt!a z!8jC7X?047v!pggukl$%?R&djQb%VEfVQGXjS0BsA-6E zD+TUZl&ISvYU)Dbt|KYUpI%u~d&J6V>Q}q&*BiAlI4X=`2lcgTv*a&BW$Ci5#K-G5o^@nv6NM z7Tw&k26W>Xm)ADgcupZ6Q;7N%ML*a)6c3CMEfM~@{0D1Y1nimgH|g*C#$k%>!9ygY z^grcz4R{PTx#WkpezwdWThub>Le?C4IXm^d z8QbB~P|T{`LX?89&tJ*`ixT9wJQ&aeo{qL^@ltth6!fbT_{Sg&GPzf}sY15NX4;`w z&3o&uQJVd}aUQ!sAj)tW`&8x@~H!zBSoAqk=hti?fw$q+SotI;9wtQRgc=noR z^hZ{KE)Mdi($88vdkc%o4>Ccf*q~!($UG1hZ59a;yKxYhakn$YA{bP^*x7Q`k(oD4 z^ky>QPUP-YEh4)fmz``dJGv*g{og`Q7Ys*(U{8v;%Z*sA362@egPJ;N@|mH|hUSR3 z#_2CrIgrD~jUcW#kU^%p8xO}UyY2FEDj;i25*M4y^>QsD+5IZ}=>OyCD+8K*zrPVd zDMdz!^tb_OK>?AHIb@?jItA(O4kd(<+bAhfQd+u0LK>vI5$W!H2H)TR*$duqUw!I2 z=Q^LWEUJy`cuU-gb~bO%lb%iW87ZG52oQrh#hYY^2!SwTV#>X$=nR%sh5g<6OI_KyYfcyJ1kGY-FiKT(!{b%j{;2 z+`Eqxg?38SwtmOnW`R+HpCkyYn3_1hm3o*B;`53)t&{cO${F_z;A8sx_mkS4bL9?6_^ zK6W&!eHtJM@;r#B*Q0Z~klkB7UAXb8>u>DSYD;=#dtN!79J+?l!2_3cKN-1J@FE{S zujiEtJ)4m_{(_C*?zyh4e4$fRpH>y=yF59*>whz?6R=HJ*b$5a=_Y(0aUIC5AzQjvoxGJy3ySHaaP<|acVadM~(E2M~2{avKR z8@G(HsP1Qj#68#7-uLkzfrvkO%ePNWl_iV_FzlO&n>kx)iGbreu zJ^?-nXbf9d?>o%-FyLV9yxlUv@P_TNBJFe%`wIfn;b5VEnFF>xLSDjxd;^6OX9xV+ zP}rP|V%a!BnTDk=-`V%hUy^W!1D1~GH+xV?TvdPzN7ATd9YyY+9vAL=#&zfs28%Q# z_sIvOU!5K>YdffbWj3-Ca8u+*4W2*B)HYQt{hKXmC~%`W8ZGuzsWuVakrF?11DE0o zE+&_Y8oZWxw%H90rC=kj;8TU~78G7$g_k*;!$~FH4<@ZZ@LRylIh!2vJ7_H_2WFtJ zQ|#(ufP^H_1x3kbO@+!$EW3NrhpKETq5nASY3T1#WysBQ4WEAo>D(;Y zl@OlT?=$w#UG@4b8w_l-L-vlow=OPviM8r_cbLdZCTC!|y$9r99=Dy-;|0TglWelR(sJ0k>p zHlYqv_^0^qQuq%qv>MEbms>NmJ=Z6$p*$Odl7ofyQy#{i&lHn)gh4>MrZ_y1z7M+^ z;!a%vswN*PO>Dar?05^Ul51p-Ae&*ZqkQC0;lek87sgc+(WE!cA3v)wS`QjQ)gmH( zjHZ+!f9wQn6(72YH3){5?5*N;MBFEw)qzdOZ>piVq%b$p_LipU#Y`^e0g9iD#l z=sYgLlYbYbe=0K=ywoZOE}{vKk z`$Gb%)5(3{e-K)l&Q^Hn98PklJY9xvd=l5Y28z;CzQxP@h|5jK9y3&+3>)fFpk+MW zU++7uLq%K|KL)QpxEV#}s0u~gpU`gk`0cbnE-{81ZYJie;=@D*h4~2nEj!y{YUTui zft_%~rm|)}@*sJ_|C^cR9a6@dWuJ^fiVM#e5af(Xq2*5YT&DT47UUQ4pP0YubKVt?>yW$%T2FnSgCqq4nh#zi*V@zHl+<}wPhrhp zLLX-CQUtxg>&zm_NDvq)M2QN}cu#&H+p1dVzmuh1!{Mk97akEYVHv(YYPWE&#O$mf zkveYEY~QJ;VvLyMfk@EDB4W8{qKY~f-w&L(ia;YWiv-SuwHd?$ap;}C+8E4xRrFHx z-Ew=y@yIrL|I{}(Peu~=QZflV$^am#iWc<{C$;LJ9TV~z6nP5tIQBe3w>BJjCs9b+ zS5I7v94Xat)g66ysN3ohPId%GOyA_FW7&25!#D%1&c~k0q%r~9Z^(sk`fBb3HRq7( zmsygTf02_LW-_&{7kN6R2a8ZDH3Bm9VNO^XBFP7vQ+vN!XVLT1fF7fJCpst)pw?%A z@L2nu&xzoM7?34^+#L){Ro(A+IDGY$2}tfdtLP;)$HFuTHWsl z)paA^hG+rXD2Q@zlJ}zrZqFbfo+ofFz_yr`N?Sel{(X{m2e%@NW^+5M$6*2K)KQfF z%<6ZOH6DR3?HL$uxd+SYQx?DBFl|%?WI^rZQhnGvakcPS)V7nM?X#)GULxrPm9X$Y z;H6$%7_i^FT5`qNq4-1IKUWaWJO~-UiOl=b=?zKUu5j|_`@jB7dq!Gi%Y9W2WmS*g zY;ahv($+{5$_-x^a1SRq*;(=xWYr5=rr2fvv$rKaVi# z`gnY^vMEgWdU2{~(s_Q=*)G=YE`1YIH}qr_;(Xlk^x(t3NeU3@;3^SUW5U7j@{9ur zbR>1fho=V#X!+f^FtHlTRm7Nz2=9tcUXC5MH*z|&&5mw8*OGI4Etrjg!V)Dque5TS5%~HTF0w^BkQSTN%2$#Q7HC0u2)v8Gmb70^fMcPMr zjrt4^An;uu_O^Yv&$bsfQNO48q)jfVMeqCY&~RsFp@QRckM}hQ4xr-gx8g+eVV?%1 zjxgrd%cywgTpcLVJ($D}W zE!l1+GEtsm&7xosl4|>`z8GJ<>H3Eys2lnhv$>oV`LfA&!Xwc4^68}y5KtuA^#6FE z%@(MBA4|(fLaFc-R>lcLV1H3;Y?^5rrxEh6&m-b67iPum+WhxP8OIg8N9{FXQ}Cn2I+pBd?U$a5y9}m` zgK_u>9x*i3d~Ejj(|ZefuVpd|%I*Csl-`>GKz06k2NU4W?=Z0`Ki!@<{3%2K@_z>( zOG9aSEhZ>{F()$#%@Gz#FsCCY!)*U|h8CrQiDMh`MGY`m?JjDno`>WC>&g7Hp!COL zQr3i3o<;8<;DfkX5o6!O{h}~t%RbTSoGCB1|LtS3zE)|O<%s1t2|8Yl=`ss6Z@v3C{2i-#ebkFG-uKiMG7n3h zFQym%y#I}`i3i28iF+$Tu&{`i?M7n9!jPrHHjdh`r;QScxjxGxFGvXoiy$VjUoF~}l&6I`1kc|{dBKb4)(quk^?`MEGpgtY5v z8?npHl>!3`!naCG0?GyoI!9^(1ClKYe@0QVe$)4)nsjrjdI_rrv;{0YwCvz z`W)s65ClS7KdWsIh)b_s##RIdaIskD2LLaf1T>2K(K9NKHHPnQ?a<$fZmbLAmcwTd z!%nMlJa}R-R!O2T7%1@}S{8IBy|cRy+1T#$*B1>TZys4tAVD8Qcr;9@4sW`Zl7U9F z49v@q(`N!IxqvRXHZ^GhdtU^%Mcn1@QqHIe?Ef7@O5>p%30hLQ#$2X%8wC0tYV(p< z*P2jC)miuWS6aBI%Jsv(E<2{W+9bgTqhyEJrq4Jl1+VqQW{C$3&i>8_m~XF&H}UKp z3dFIy=^kxHFXrL*Xl#j4b1cW)RbN3t7>Dd`h$w0Nu!LQHy^`M=mLK;s>X>}vyrn6|Rc~^B-FNiP zLwz>whTU-2qSH<_V%%+u>jx)^*7?SCYC(N8HG`s3|LxjG@qcHZY0yrN#C^xN06jP3 zQ-atT3J{th11rN4nw#r!^sXK@AXYap~ah9w); zCL6m_3(luq9`4OMRGBJu=$5_Q()WwjFU+L@$wL-O7lo#GQKmGnu^LSqAIl6{UG*;0 z{~*Qy1I>CVQkJ_;KlYH^EQ6PDI7`g>=RCh0DpyvV`uf4P?;zY!`%?iI_pRbcxQ=>4nr1zBfWd#{NCV#ac7qk)Pb`u1(1b)Q;=0 z9tqUt9d!#H>paxcwvJf5c5YvJ!X%tLl{zb?epZTJFKJMmtGt6nXT=o%MbhX>-r?F( z4{016jj>K#1ET2LUB=V#d@j<8AgM#CD6Vhb6_xFt&5t9SV^KfCKNr079NEm^Fa{@2 zsQMGn$v9qm-^IaLOOfRyRMaF#938_IMQGB8mIl_L zDbt?j0C5e|ZCKYc_4ZbaE7grQ@$NxrT7`;@<8S68TPpyA2G`j9O~i$YmXRF(m$(?s zsj3WSVy;p{SNuiq#J65fzau4;io^&IxBGb`ey<@-eQzB54Z=!Tff9Ejm#CS{&=>Xo zc>&P15UK@DTkNL}+1bZk7pjfw#)6ScLcf09!4g@slFZ&nrvsQYa~f2TP!0r9RJSzW z_2Fj*eEp<`6N7_#tr1@gi|u6Lkf33ef%Qln{}SqIZv~RvJGF&vHrBiY5*q_H$!^&7 zbPLrna0hFq=n_o7c%*b4VPP87yj1{X_Itec^V4Z2vKz`5qhtvh`OXh9e7h(;uua%8 zZc=QS^6guvgkN?1LF57{Dqw2niXk#_6`_6sKoP5cWf|gn@MTpXaAUR0_;i=G^LJWBEr5*msLoIMC zPS72!oiP+^hIGUCh(EK}<5-BVmCQr6`6BUqt9negP{yY3HU$KnJ~;ei_W5Cg0bdR% zu7VxiEU4c{_zMwlnU&b8X7XYe+{k)Gg61;6s&uA|FsT2gy$twuE|)X+FZ!kEM5fn zhxAOkcn^Nk5WS-w-cU`KB7ik#N_;Z#@Cm;ng*L>{R1Z$c+ghd}ak}I1K$?oTf*DB?AKtfJlB$gTtbjX?+9f8=GsLE_o_Sn1|kkQN0|Mb-y z;hQc^B|hEjjIvRQ92GUFa-F=wly5XH?}YAP%>^;$TA>7q3}s+eXTPPQa*u;aLe`oG8Rs<^gIC-Oa~0i!!cCM@=bNS}MSP zeD#(hy%Q}N0D8useBcl^4HO;b!etA9UhoxJOmMPAz2zVT|Ypd@Xt z5=M2<2dsovmO>y2iW_)vvQ;{=a5@K`Pkzz4cF(u9_1!M$f(-+19+kaZhSq;iEN+mG z?=;N%9PA`xiN6}mK!th*R(a<-PR~nOf|`~b~C@^017UEQ5M6YH@g0V zrm*kFYE`E5XI|M~U-y`rWG$GbVvg&gx$^0KeEjMBc~OgX~&dtcf+uggz`7Vg@pM}ydeS;<@ z%}i{7Jv^XJ2G)e=XokR+uw9#?_wRqf|J)wmBbi~_cD50x;+;*6Gb(`&@tz^j1Tb)W zo!<^2lCa-BG@vIUa&rNRRN5*=T@mT)GQzoDWTd&7WD>H`m=LWUPFH}P?xPNvo`W-o zZ*uYK-&NH^+3vsT`A|t{X3|oC7E_L<_B%8y#(bjO4F(Hpj}su+y~HW>j*PNcb>a6> zRLrlOTawr^D9Zx5_i8+_36j1Y0a$ZVh9SBrWb`ky6-ij_&qK3TrrpHIA(kDhtd29| zQs@Vs3q?00GLk3L7$x!YwKFxmj$(7LP@j}5*;eH5_`!N0mBG9eGEE0TsUGBy4L|Ij!c6Ap z=hd*Is4j54^2R|ODSLbz zWcS04v#g6G<=+KoRj#_)+eRX9K{sECr?7`7b$795&`l4`>0MaL5|fQqs@LDD_xv)A zFpIOy7fHjn6Z}9*Ec0gqQxofbIN!q#D9okfOT`TO8Dq5jLlZeRq;C&n ziBDbh4mCo_=zI4te*aD4ycPBo_2lDMR|}YlofLvkJ8$acL6!RNnAmyP{2f1x_L3~N z+T3A+)+T%K)CulIOJ-5{0dC8gXCH6k%R7<-Lc60sNOH!(l6>-O{x}`&*ntIdNd>RE z4iF)gZm+ zlON*z+=D%_LR>L3IW&ihPUOa8xL;3#K}P@YEx;omADS5Ifh{65(4Pk@(0q#>z)|*g z5xH|3lOU^8MiFik$!sRG?R^?kvm0P?Xp-Jsz_41!`?J2kt;KQ zrJVu<4ko6EgYV{<5QuxJ?`1JJhuQBFJ-2%urcb3Z2OD@vB>5aj@y#Qn>|rM&VoI!V z6IqZ8JRLKDms%^!r4ofLH#ugdyL&rzddLZ$w{(@l@4TXiFa<9Bly=kzhJTDZozSHq$!XM4m`eA$#)l|kJiPr5=MEg#(lN7@aew5iurJHfY ziq~{BteVmW6jZi6Rtw?&4s*pAI zkby)OG5}rI*!JoC>%*Sj&qSb4f1oR4k*jH z>Q5yYR`>UYHU_7+Jt{lU0mNjZLRwTGijL6Cv8s)~5k;)LB1peK`r)r6BWgHQ`d8D; zq(gm7^fOJ8oz#`X!j?-LjH^${9W@43f!p2?$bZJy&7vvY)K1Mbna=8O6uzZP|# zM>m#aqb&&7OAC9-*eY?!WDjA~UP9W92Gc2saH4u&n#iyo1QC|6O!)Lt#NemXOR9S1 zt9TZ$DN4!+Md|1BemOd_t!9a7b-}6+gwQ;+u_&ywom;Jrul|Y3cqBxQE*9uW{#>Ov zWbs5~I8?D`*gXHa5TTOZg^Q^#Y+8xWR3GUkRiZh@e(sk}9B?1!#t1wVIC9Y#DtGI@J0G=(WjOx_2bL}W+(5!1X#)bclWcIs# zpYv7jCApH;M3rG^Tf`rgBcOxtNDB7ZZf*OeMSiTnW1wMqtcg#<^_2g}C;h{~;#Wo4i ziqw$XE4#w-mjEt6xiBVJB9)XMKhhrD3ylpc&+Ivfric9i1C5=fZj9!eeuh!lDRaiCS z^?r3VKGn`RW%X?H@-o~zU~y!n)?O1^Z@saDc{ z=LX<1H0x*qNxr=u(HM#TTrxjH4c4} z2WYrQ8^%DIMc)+paErzhK5Fo5-$QUMPJjRlYorJna6vA4|39XWz-=lHYaJm0P)}Z($$A@yP6QwDvmU^dLEAuZ;gTgg8D84rIAM_-D zVoGAUkPbqKrnzevE%ICs@`V^F|Iw<}>?vY|4di=tOZfqErrH0ECK4A-i+<0Y`V!a; z?<^f7{r(wrBxNW7k9I28*5up!;c_I^4n}?9qS4rK2{EF3y~SedkcG`to9Ic}zJC5; zN}R&JvJlI)^XG~gG^;FBo#VNN*w$(&SCCONgZl>3tGmD9Kgke#^Q1_S+?zxOBPrbp7>76;^`%x4#EX`$GSkv!$5a8O6-n=}E7gq4nxF&-K6 zL3)!BTv-vGoeUOtwYQEcL#~6IFA_w3dfXRZLg`&`I!xZTevsf(ZNYyC$6 z_Zj7XbXv{O;l~JcIwN(mWkdP#FWUi^Genzxt?r={TcEZ`zBh6w z9y;juJz(U7ZJY_^tMB<8Z4BkfQQT>WYgU^XnxzE7eY-*>)1qjAF=#2lSgZew%kz9%xy?E2 z`LS8Q036fKScy1jQQr8g{e>yOLg~>a<4;)Xrr1e?&quF}KURkuqoJ!@d?r906Rcda zg%Sr5dKLJeXp@oV)tU)#QDRQWBjz4W{e0AYi2RA4DzCn3LC<=}0W1Lum1sIPA@IYg zU8K(D%3~n0R-$UP|Kh6kgiiOTg1VI5r*jtrUsmj0>Enq+lL~{i$ z63h^+Y49X7%m;Gma>tHzl{wPCh>4bvj4O>J<8e(-nB%9GP+mgKw$B9UDYjy4;R&5u5IMw!ClF7bR`_B7En=-`yJfbw2+$at z>L52A>&lX1nvLF3G>H`^OHBAb9fjsxVYJ89e(^aVq?}Iyg=8_10jM|4p!o_f0-;LB zuqJ<)*~1Q!Hu75XnAIn|eoKawkQWO2_~Hu z%g>UTRbP~Fj+9n>r(+@_vkO?ksGP%BBT`zs3W{($hiV1PgOW-fgV!NOc%utE$+8Bl*KdSzwd)?Q zzLFmfQK>=m0_U{MSnu*j=8#B+U-r1O8~@skj1UY|g=kXN?zm%;_<=W}Lx#ohBpJ*7 znk#-?Il+!mQRVowU&KC{aRIVO`B)dONGsb-GPGJ#nHv$lIY`cb;th@ZP92PS zOX_xWv4}McYTP)>fW%JZ3={(z~o}Y^>S2brFO7J!3!Y_xSH{`^JJyy!;m2KiM^KX+g*dgk8kAXwO%+Y z?fbWIu`OSM^etbcc7L!vV^V%a(&%uq_HudA=&H*U7ZKn>yIG2#u^-S`(;1jpFG3Dj z{BJJwXqsj+(AC7|ar0vr3>$WANdG0PVNQcRRki#wIAa@TBwu6A=-#4)I~(CK;3ztz z&74tU!5z}ZF$ZrrdMn`TEYb83$t+fGmRr#)46M7$H$H_mb=Xf433RGpO7>#iin2_$gS8;#)*lt9UOPZmu@h3(eTKryzbITsxOO$R;Nw z9kn_HsMkHpm!>NC*lagd&70U<$PM$zejlk4MJw#pyyh;kKp#;-fBid0lZNK3iUGg* z-hbl>-$8^;dHn>Gaxbx}z?;41^afo4_ll)zeld2GSs1?nQ(DzcL(oR?gK1jERQmUR za6Yg2>}))W8pRds3v{8d&3??5nDK$xyzcPQEsrymvbJV;_RKj!Se*G?y`HOQWkH3u zc&|xN;ZEY!OVusmPqpM>OZT_Q#}QUb{vRyq_&nb|QR}^Mp8X=So>Jr?Dq3m3<}910BB#58RV0>M{o3Y%eYIc$ISN zC)4GX$j}Rw@3=-Hsm^npN_AXX+8wuL*mQY$Ptl9bXGjL0DC}kwN-jFgWN_aS zAC^UmyhdVSn9=6Q!rT3HHn4C_GXC#I6p^E&1~R?;wWY!R9jnh&u^S=Lp#y|vi5>9MK`j}3J=r{kL>}H`NL{+};>aVb`=+CJsM;C(G_DanB!Pgf_BC zLJERj0++5&dg<&FVi%6kW@WR-)l6#>Y8K^V^sDNx*vFJrkH zpbyyeq%iNVZ$w)Z z$M%%?g6N;DV0~31ao2Oz3hhI|3`D>M3D@FeT07Ln{$Pa4e<_+_-ue>`K_2(bPqVupVli^BSg=k{bgtIoI7`n$cw-3Q9Wj*AB9O}%i7y510A`s<|0CX zNLhVhXXHw7y~BOD(I(QRY@$E!D1s#l_KAWDTphlr7mnkUrfP*0sZ~ef9C6<+ip@9n zu4zbl`}NQ78^mN;G4$h%MSkg}*$pbmK}n3jqdva-Ce&V#Vf^pNUJx@XM5BtLS}PZBrb$S(@+s&DrxOl9COk$ttq>NYyZZi-5%=8rz!yE6I@&FVcwK6P4 zf5)bq|ERr97M&*^MB2wIKZo^kogAJ{Mrn-z3{00$)lx6C8SD1`&{Vg^29L2u<`Xf@ z&m28}KJj<Gsffi$&(3{S=xEN5?ROx-X{3XKmUL6F(>sn%|Sl;MFku zoH*n4?!y^JY_+!$&(=MW=N9Xw1hSa<_vcOT9j-yki8$~50tidu3~@4^B-ya=b$rW&xh zK>2c?3?t63qT}gYq>!)!o?E6N;c3I^zz;PN?WK!9h@q03{Lob`NiLr{f zP<*Ci*jSq_VpwPLtD1G?1i z?i;XD_STyJtu?hY&5ccFhlz#Hy}b}(f%!Dl`PNfelzY7^DwBW5#P+03OzA_aEgz=D z9z&7;WGKQN`^rSrp$?oa=hYw@q;G}7C=slCFDi05n54;$BcTZoK-{yb-+@D6OLB1{f5D#CFWRr8|xDImXVc+d=EFvnHm31NRx0w9EW5Z`Ff3kGXxwdVw?8h{! z*{AKdvvyjB0H;QRJ3T?AKb?EN9*;(~8YT*`t)^*J?iH9XWzj1zn^<5 z#HcRQ(B)YQx^i%Yl*N%XD=U+!%&L;PnPVPxfafWk`CBZf76i&4V0fP z_`VliwjFcjQb*eT?_6WR?$XQjS@f^xwfs^Pa1r9>uwR7%CP_d2`} zgUwaN9|3QnG*8jF1RF5*vC((!DNtnv$BHw;Ph)ySL(&#ZI3oI3SQ4RdLzB{VgM09; zO0a-ISGr}Hx^X9mz0{hMn+iYtyeoumCiXxOTZ9SYNGoJVv&}S*OQjomSUs5xt+#%{ z`}!g?o7hz+T2hSr?^UJ3Lfl~Z+_H-8YN#Mi2MMlY51ZINv$3VeL{@CSbL%f{OD5RY z^XU;b{FpB|GSjoZ1?x`~#;(U}sApR2xxyA1IKXaxa9)1!?5IA}=&{fMZd9bXKn;6j zdl(M}xnx%U&m+Fyzg#jkTRaPD|3(Z~igNUv%POa@x;zaX_C^>Lx|(!dRrxSbeZqMS zBZmh>Fy}Pr#z;GHD&c~@XH_IFw71=Fb;a;|+EMq)p`@_*{i4fmCGA#=q>*~%;YcqL zcbSJ=X`YU2zG$L03rx;kOKt5M7|}^v=;2i(W|V77zuFA@e(k;}gNu3EvUG^}vUnP2 zdOzxgDP}spqMg0fKODRv;a^44w8lJj1>9W++hSj1zn4Es-G^Pf(#v>N)nk6jjmv; zg>XRA-4X$Pm{|H*O3pX4For+pK$P`AAJJ=40c5ZlaSc)RRM!KDul)f>337sH z7XAKP+&5Q&-SvSKXTqhAKKYi`FjxG?Vu~iOrG3DD~C~Z?H^_i*f_j_d(MO94AzjQ(;v&wwIWjm9f zw{ss0JW->;!_3nza3I&&&Hl|xOB4m>86%gz>V+PLurZ6J_%(MpeyK5k#X0zV;3S4( z&b9M4$#In9f~Fse4PQM9I%%_8<4%e9sk#B6n=kY3165M4xT1Sfn%&XXm!6bHlr zf(?`^)8|(c6VyM$^rDV~24FN>eM;lnj*<)pkMQw;SICXS&H>;i@yE@`9fN!QfN?WE z4wEU|_yF(#BBRR?ESP`o=;R@PP>|UE`Cdr|lkT9Tz(Hp_V(1^U$;J$tEq#ORlbJu2 z;idfm$13BU+XP3!-FGeBcN%orsURJGnJyo1KHhP^qCsyowV+OPK;p<}K{z;|8!Bn! z&oe7hj7$_C#gr1|3o?Q;{MUu+eUNvfEs5389$hXaoCk@=ll}%psO-L1-lf>6wd zm@&Cb2HR3H=*1`6Llai0b=5M0vU<5)eRlLm6K!_q6w2vhBT*^qIny>3#dL+$ZJz7g zYmj4swGaudZKkTUobyltqOp6o1EQo0$&GmRFCJY z{cCW-$c3uW=n{!An@Bh!Jwob}2REbJCjfKmcGD)6<0hiI^KU`tqU!gLI})}*Q}5XV zuEewn8Ecs_z2Q6&_ij0RgU&M-?y>7R*l}G-H0RIT`aJqo zvpgjnv(3{5%Ho7-tAlRY^@dRvHF6>s8*0=4$`#<4OD9(28{8fSW4Cl$dg<=_`TKk4 zdB4fP&iU!Hav9>?S;OCZzl#@S{hjhIys%8R^^=i!T_8Gw z8686lt0qc4GNTQ;&;E%f>ym#xJXsCX57tG^2A7tt548Wh7<%y6A_F& z^TBbMK~d$>-u9MJGn&)L5Fl<1kWX%2LUtCIwNGsOt6{MO@Fq%VW2tQiTA#-73P*+kji}K!t10#V>F>8+Mgb#(r91u`^%fD6GfcU zIV)m>$HIG6;dJjnQW*PwC%Y{DdUHW00wbBQ+>nybf~SSeW!R=(59G{{_@>34+X%X@EIrDm3&k;-7 zw_Sb{5LvFMQ)I;G@CMF>?b(FsGUj~xeU3q^t+Jpm$vPJ9^xXOnfmeD?D5G`a55#xj zuXGltF#&JP(@!pNkVRR^CunPIg9K|$ief=s1O8o`{j-BVU2Hz;=iM}DfHsSbZvS^( z(*xjehi)bd(+q2FKIwpWEZ z-hF0__NZ>cjkTE@U2SKx0pAI9FT>OZH~VNEA*afv692|h6)Amfpim|=~5^D%)X2g1)4dk zeMipgQviijQD7M;d?2@pnPsDTl%=5kmv_!3Zh(^Lrk*<#7rkT~C$#}Z>(=kDb-Wrp zUSYv^thZg`w!Qe^yD#&}NYQE`N^T=Ms#=QZCh75nKr%f@mpP_Yn`%hvAWDL66(iOi zNIU*cjwrZ%vAY)$wg{gZ-xwSMu?|@^zXm6ICBB-6B=h7I%W5#Pbw95X@*6|*QQoAZ zyZsd4^gFooZ3j~*_xt)%D+w)3Z(2^fi||+{7so+E=+pdTv798NA}-2cyESxCaTBkp zbt35>ISx9;J=={S0Vb<)!v;IX2qR83o;dPAi1`O{<$2i@)L}m2+IU;ulUj1*84>BT zsAySa0w^(nH^i!Ajpy?Edip!XH;p7L`+@wV^8_WMxI2+6bnVUBGT&M>hRBAiC#l6GsB8Jo-GW-FIyNF(QYm2WQ!u` z$R`FC+;o)~`~RXcgFQ2=HYk~?0>kY0t-e1R&f@5fV$>aTmJ1*dHJ}o%Y95lnAP}Ux zgyh)0DAtRUfy^#NX|zQp#lSILf8=p>tF9qiSSW3<6PS4wUYh>Kc|_%of;RGq_K!tN=^%`C9asAg;uX_gwOdC{UO+*T@_offaYbVCkv zp5P8+XNG=NWJMx^QR;BfWBqefo<`7;Jj3e1N8M&cH^QFrKU2bgZK@x?@@u_XkzuFi zO6`cN*zDy!T+;6i>i&s&jEChF)%BSGv0CgboFE#hK5W0J<=W^+Dse0gk1fvp06)9LGgP2E_1p>13~#V4~E_NZt^g z!UKBTti0}2b3rynCrA5hBwyzRCwjH?BRYXtUS1ym^S0cq=mcU7V;N)Z)Nxy4Bka@} zc^AhEy(9B0hB?Y+83QzI$hWsjYZ1@r-3%9fnn%@jPD@_UogSkkscL zp{|J(uphTF!p)1t{4U~H01gOZghSio8@~_MdqKw9IfrqfSlA$Ob+OXjb73KQk^UP|14Ilx#+2``{y zx~cGt<>d@i>Gfc57EGdE-h4hM9Nav=y4r9=zi)fKxN7&Gb$!#$M~vz-ujx%2tQ*_q z|Nhp0MJkbg_IZH-B$Ur$QaX0av%9@!`{Y*#P=;)TuJ71{YA}FL56$hkxJ@%&QjX5G zv)%ca!kP0X*8vPqaPDz0A9`{|$Q+X=`g=#dT-)B`4pt(K|3WPTOEgxa!rvhO>O!-Y zs==hJ(ENSqKp1uh0%H!2EisGDs;4J0V4ZxD)_r3B4xZNZb~=zKStN(EgZ(g-H$2;% ztAvmKZ?v8m0U(B%@6UusZ3`El)}Q~w$MShRoX;NZp;*K1`$dEi6eDQl>G%lDr|mS~ zN}&WAcIHT=;Ut`X4GO|o?u{0!?%{qgY%KB697Cu}Y(9h;@7x!$bn7CoeQ@h1!RY&S#kzYxs%$$4f z5heuXRIz65q)5GkNw%PZRq<`*-m}Y!!@G3=3_Ca7r+Gn&4CAkwLPU5>9~)fe*DG9P z`gT3+jx%Z(<4h@A&8ZI7kR3f_t_yQ z)nFLzBYm|Spxi#6`jlnsbvnkU4X+1He--Ro(?P{w6g&k6Kp~OO^C2YRO&?uS920>U zs&!j;lu;OZ5htE6-PZ=n0t@{CDrD_&`VdYZE!U2ryn&}TNW+@Z$@H%NT*ed)_Iqnn zh89nWZa!NDGdtG}0+p7x%hKehkgBV44b^igI~pg&!Q8o;3`O=r>R9K(d=LHPNx4O2 z1Wdqucp?8-IcI{A58ANQKr~??^AGW7+3_9O9?@o-p5lb=^l1-r#uz153o$N6A96GT zpgF_3dVN@+HSf&u7YJX>$U}#&uZm>xujc5*+30PF*2oF8pND;^Kb-OR6tA`Flkx?+ z#qNQUM2s(Ro8Kensw(dx>90R525v*w87%=Jacbs(hPToM~K}LG-bPGa+S8X3t|)_;mvi z`YNvW)re_Um~kv&p;sQi(Ud8_RDU|TAOBfd1fdRNR&haR*nsp( zs%Z-s{V;{?5|zVQtsMD_uZ#2?8QqXsoqtoK&2-WJ z1U!(0@Uzdc_B#9i@!U(n@61dpbmNFoXIQRP(&S|yl+nA&P+;-%G6Q^#PY5MSrvoij zf+oWGXU#Kd-t6Fe{o@ul{zvk z3@)q&eo|P{hx$fNZZGEinZAcuFUi39iheE{{k(qgGN5R%MFT#>X6gLJD$C2OgJGgdzB~bQh}Ru`T+){_@I3#demtBti-GvZVs5B3v@BoXuu-WETaC| z6&%HA<7177L#g`dqaRlo{zwM$rr0zuo_J!!ZyOq?)MpEFNN79{XBQNed0K&%9}Uv( z9-MU~zRx}xh|jeWZy6Fu-+ezvM@Zxst`G5hu*2pKJ_fk{_PI6a!CBRYFkQf zN5Dc@_Vsh&!?tDa&ExVBhOZ#^H@UeaR{jUUu!+uYjunqWGV2VLabSB^sfls4f}1RJ z(30|Ntw<6GlJ_ecNyrO#Fp9x4)mmzkfqJge=uJ7n5j}5V*yHn|MQl*U2(%+W552;! z67oQsT+6?-YKoHm78zSQFhQFCQXP1}0zq29!T`MZmt?pix#0v&wPglU`-a1LCE~N* zrWWkYyR7kO&*a{!X9TL>{xUwc1)!PP_ z`%Y%X>IL#JLf{8yds|qY)d)1i2T<`|+?tS}PC-_x$!t;Uqvhse4q6J84w1=#Tugxp za@CIPHZ>NGy_mZ(&pADQTLYEOQuny-c8^o7>mWX+10}CqgV~o3R#K1M-+CTQQ@Oi+ zmRtUnz_Wu}h`4E`9Y%$6@raC7GCNM_URn?9np&!9E0PQV{9Dy^2i zQiZ>oJT@f99ZiWkz9kI}@S?8cjamLAmhR|_^F8%5Nv45!=ksMT*10_f=1_ zT!kS_l7&5$!}umweIkrca>1PZdu1HHV@PoCWTt4=w$#@7Y+_Z-=%M8}*>;qnstU+% zXmFelP*^0Na^#D_2nh@q^taBWC^46^rns`K&is> zAA`l$J8ana2zsBXt+)c+^_G$Xhv$}Exa5u0lmn$SqqKNDl15jz&L5@PtuNSfDviI* zVq&ercO!%pGr5;(iHQZper-DEKn3m=b+4?4tV7PWbIu(Utztku7t{nxXE6r8)?q3> zliMSI&nzZWx{4;(-6L^5!vec2d!57BJ;8077ij#!eRZ1pGeVw5|HwP+QOkL-V08>p zNLK9aDW{J9BW5uH-LFGYDeZ9x$_QgyP#tVYEG;T7x&+^Q?^7vmo0;V*vD_GeFvg89 zJ8{Ottk_~b9++)AvxSA!-(=#;+UZ)QaR%k# zbRbitxTs=s2npa67x>nupQfuw5J1<+IubXDUNQG)EdGwknP9&N|6d+HaeuT9YTrP- zf)rQp3gG;|eV23+7`HP#6`&woW)?2Gzy>ExAuE1@276lb&h6^%W8DXp*5?toeEAX85Efdj2~zbAY>U`>8%DPzy=uu3`GN01aq`>r-wKzo0W5HFz7vIGd|wPx zOw)9q7^seJSt2l!I8`N~270|q)gq*Tw6H2xK?AfvX94Q%IQ%Dj2BK#S96&n_K?%qy za`0F~YKRz>I=jfd_`$8Tad?+0rSJg;;_03{;C=j<6}4XDnE8%yTp^F{H6dXibr8J|$e?#=j5&TR$Ct4lwkzLhI)q zByS7XD%Z_G7r?jjvYL=SSRTu{XcncCK;6FFk#B&w9hw$9sRAO$-dGzY@K(GQ0*U>i zbrd13aHR)W1zf-n7x=<|446(q?IYsD7r=uxXyNc2w8TI=ae;2+Kx)0FD-3MaOw~h8 z7Ji9u6GY}?ms783!dbql>^^4)5LV3>wo#MA> zRJ29xv6(32LQx4(ST(t*k@6V&fAfIF&jHMb^XLykzXoRurid2bKV1IhiL|!1Bsf)4 z0v9k3@hdON3Vm*g8%*lisr7vmDW_o~!QTqIV2lCBDSfG=HT2X6WqCwYtC+75C}~`t zsW8w3QFldhg z^PSxdmcyYtn#LmUyL}0_^pojQ9nSZ2qe=rtPapAyN`G=P^!5CHbj1F<2{Sv*t=9zU z?il<}Q{5EC;5xv@(<3(AU|;(1-HAIjy#S-@;7!RY7fx?dScPpKH}erpa?%s`dQ@}_ z>`iVY++t|`PM(tn$BbRKuw4f9p>2t&m_VhQsy{rav$f>EZ3QRKtF9p3zi@55_`Jc2 zgxLYMYas$S1X{LO{90a#ewr(JyE_~{X(051a%H~u&ZU=hf_-}M^2vIDzZi} z13qfqlSvN7Z`k>Gtcciz0)XqX~+LYlp|Sg1tFHbx_e9ivxp2zT0+Gg-Nc!kbb8v8WnpIe&a4B`ECEv2J99Q zvI(_dyw*dL#tW=>6#WB5HA<^Pa&0}4&`94EO5}!xfrFdM4y&Zfg2d@zg}`!xfif>k zpxO;KfUUcO9JsRTu@@3a2*CDJeiT*x052|EAu z5N&mD3cM-ds2LUoRk-4E2(m|vty5z}4T+tV=#IIR5pX1Nj82>1ev(4w$lvCO+DT(A zZT-DD4UGlYFg$AY%=VM)LcmjLY*Q`tDkBzDBG;{FyF|H1e)Pb7{1~YHdn$RP6*iBX zD=t7ffbA1}3NnVtQf{P}FJqnnz4-3niO%Lds$eK=`U?f|x$P1Z!ejy@ifN+Qd6%w_ zqIo{-H;7}+`&YyzF{uW5BD+!(3_o49e|~fuL;$>q9v+vGIum_K1^3Z~=IQ2^abZI> zdYXZ+Nl}>#SmoF?gj^&jfD(D!HXEdtIv@;;yZKQLWBEMcISOnX&&7dGWia6>H?|p8 zg{?JeAC|7EEC@!>k2KV%3j#ur1a38Eg$-EV;n1QXtW;sc+gD5&j0QI&d(HhaR%(hl zni%uhj_g~|SCGSF0Q61F86N}&tu?)aiLz$I>5YxiG3C&a>DkE(ww#z~P5F(VVxu?y zstXbydUtsB7BH$P7`NJR(*x-&;f7nQQk!)7lu3fXsEp+$X7FUK4HISq&kZA$P$$AY zgU6ynMq& zG-*<_6Yr!Rc9+#kYR1!gX*cyexY)H$$3W+(5RPD+6qa=k8-}vz7zH`%bMrL zkWh-;P&J{q$6?Eoh}_Hrg&_nYJUq1#2_9)P(dc&{3ew`FWR=XRgY;WV4~MbW-9Gv< zs5Pxs?FjY7lfB$;EwH6iHFEk3ar;E&Hh;*RMymoCvyuA1@&wt_cFjE=?B8BwF5-!m z@y?q5&_PPNh13TxExT@V=ez{SgIkLP#IKYG};TV{7M3`l;?kmT?F5((k;#q+`C{ z8Eel-D#3e`!VhGU$!+6G2U6c?`9-}3(g_$Qt2ivt{0tT(T0IY4-un7+3X?_=0R7s0 zS40C$HzW%sM+R>;g9yIE(MGf>8R20NPH9Ew``bzW>j%{6Qh5W990{RGYb3Ps><@3P zECfm}k)&~vq-RlHVc4z`-EAM5NbuOk z3OMl&LL^uD@a8+jL`d|s^Vqe!@LD)ugUybka9+U}PC3h^wv0h?N-$%2;zdYFL;{kr zM6-rFV5Jhd{q4C=8pVxUE@{%=nSP6$Uf7Un2TWelSZ&pfQ*%0b-_J0C3MhOzT)-ra zxyWTU<)26N)KC4fSQPs8e9-9{6<;XTvG{~;0q4GT{lUI%LSsNl0sIk7h8^-qPdk1- z97-@Q4o7QP#HgD0mFa4BRspNq-m!51y(B|fEO!yys0u)EyOp&nAUpYH$OkDhjJ~A{*j!n zebOcHOUDHQzi2QV6#jHHsI zmA`ooyUPkJ>R<%LbW#BEY35$7MM236Be=TkC|%aXJ*bR)e|>VK`hE$*6EkYmD9;GB zHUqMHllpFTNgbK)PcUHO&}mE3N(%?!0)<>69ZB&r3s-$Z|8R|CdMO5Z^DsT{7}kMz zvnU=l_ybhT??JEdak3S!bArS%1Qc8NdsE3XGjN_W3^4X$cxXTvyf*Lk3}3BsGtVZS zsLSx^66I24ttIxIFD^O}i&19-&@Tzp<>bkxV%>9`rpR6=|vf6q2c<<7_8)af?Yf%H=ARrTTYFb21-bWdE59V2({e)48S+ zaZ|QI4wW9;B&l=J`Z^e%HZ^>f7yDCG@@7GQPBoyMw z7LPA+y85hK(de9uO4w(j62T~{zrax=czu62L+xons?~_>c)*%t90Ivi0-#5=Tfi|K z0FakZU{^t|n{`hPFZKc2c4m>%iVwyItKr6uVX14!LHL^T zEkiPzG5=Aw5_+Lqi;vt_J@6efCive>=zDsC`z)-i0R#t%a8%cSSb(IHocOX4NDzcO z^UBRjJI)eOT1LS@V&m1T;X#q$QbQ6PGpgA)V>Xjg`tBX{`hPCuMaaMh<~3U4(0Jgk zbeJ}tX>wga=Q44;K8pK!m<|W(;c+fw9=#)W+P7!H8J522t=qJE!vyr$pq$$$?1D~6 zdg6&dHa|=IQv({TljbMN_ zw_!f{yvLLD<~q_kDDR2ZpkynDf|-lIkG@A9dSa5sPU}y&VmY4z@7p9-khARt-hQJN z9554=5Q`U|FP&6a2jo`=>pd!0`no@e~{5UK=Vwa{m z34))tnX^yuQE)xv%0gs}C&vA$Bwv5spqQD?SOWVB-y(TR8E7a!EutOGq>cE8=@*5_ z+C!CfJLsS?-ioEnd*zNKL0>%A6~Py=(rG^?GYIl6g+fcEP|r<}79_W4@T`3kNdx!q z&`i^6opW9HzjeKm#C***+r_NMhyw_8$2BH5l!UUP!qC~cp@V3!q~DbH?zqw^c}n;- zgOg~x5J{zE%8P-ab;GNuO6!L4U58CUx(lMBAso@0aY6zC4mb~>ssFQZjErzRcR;++ z%veOUJ1WmkRto^S;6{616S;GCD$tT?@Bw3IWJIiWsDl3ir3E0+ldeO|t20@$zmrI& zy&(y7(=;r)Hrca3N(~cm=|WEv7{&FN9NHO^Tf)O#6DaY%O=j$by6Xh9W8Tyk9@gM> zQw=Vq&Y{0q7DV8|5H<*4k{=Px>R6On769m};zwIDJR2rBl5nhW;>H85h>_#%2a}jN zDO)zJLWN9$9yIT&ma2r$p3CNhN9ki6>idK^uid5I^dTp*LI=B`5lEqzb8`ZuHWYMX z79GuUKGwm*FRg8fNxJo=9Dk>8!t?O{oKu9c)i^aDcIm>Nez?vc?0s8;aW1=`J1DbBEIB=4kbGVj}lzK5m-51*-^^@ zpS-ma@n|Eb;6S;*qxM*&+Ytx8l7TZRmaazbN<&A(ShP)D_JJ` z=#&={*A5Zn(lBfGh@8hy4OeoYId^L?k;lb5<+)J4SY=O}>`3AwdF?_Cq{arx#9B<|OaPiw>K z&jZetr)7o9@w&C*^RKqQ>Lyliz#~KoKS1sc^R{*UGH=vn^vtt#K6Tgs(#_FZPKywe z#d)umN1NeBM={6J(cusPoNZQ!rwo+`O6fND<4+?qdd8_`%FmSc&^!5XdrxezD7`J$ zqI$E;kI;rG9PKu(&HIXVi>vqB<=plyW;A;@wL%2Z`;M=0!J$zBep?zZXz_EdUg|9H zX1b0e_wd^rb(<>2NCNFuJ>_@N+Q_%DQXpuM69mOoI_oU!qULPY<1OzwB}&12jbM+R zCs1qphWdVTaNWCK{OmzGf($i(sCQ$mfTx-i?K7BoLGwe62nf7PmbH~fsqiiK zen^o-Vu~ZP%eE5fBoy4CUg}QV5eoI@j&^KtLwXO7d|H~ z2aX_8jrUx6UGi=92OP1^ihA zKR`o)naewDuYsvyDq!hX?=2_47q;j&2eltk<~mqa__Z);`|2g0AFUdk?QLlsyn=>& zD$;-;eTgymKCnxiR7}NF;UP($l=aB*s_ zf4ND34X=;O2w>*A;?GcK1k)sE=mLtHD|#puaNHkw0Y%^x5MRfwss0u6FV6Qh+=5s9 z5D}nAP&J}g*hKzW@u5mK0}JOX-Ox;ozTm%*-B(PszH)>7?fn<0x+CQ82{Pt+ffnaQ z(S|)GKRuB6k?wh?c)551J zbdi+ZyY*Ic_EEdc-?A!)=3$QU6z*R2_CZOO+c*v;%ByO;g4trh+*RsP`^dz42Y);s1Ntf-mz@^M zo)@U zn8duZIU^Z1+nGM^@m6xy^3ngkXkjG*3;r0TQIdOs`nT&MrcY0k05-_8piBYu)A$_W zr@0K*2?$@itJXCYeM;TDulJl?cV4)y;E5sslKUm$BtCxy zw6Xb^!?0R13Bqt%{pRT>!yd=<`T@*qb6}4a%6S|}Xg9B0|38*?o?l%$pdN=ZE*$DJri>7^gth_TfUhY`C_Q zUR{HFn(uHNI6Xnt-`=^^|JxM^-n44?ND1AAf&d%@G^Zi0|=ujrch4vOd8}hJ!xMs0G#lhh^ zn0cK?0^B~&F|C}Cz}f8>9k3;crF{290zeZdUr$(AV61^=N{A>M2gOeb- zvFlGWRqJXa1Z?%cRa$uYS6`I2Zh^=Pz=QTvk@4qb*yTR^MLD z3@uO|M%r7nBZD$(^o{wx&N?)Q0PC=r`sPdhVziUnF9Xp1?;6sR|8c?-WOJ*5%0zMG zyf=uEG{|_pxMNT=g&X!}@_N*dN_lFwaTQMqU_uUdWP3F!^1tG*;z}2e(?b^_b4oB2 zspgKqEpIimCWRFS90;0&z^H%!9KKCKNb(e7rw{n|KbC^HsPGnpiUT`1%43>Tz$d zAH3i7=&}Ch=YxpP!6u_vlOROznIZpfZre4S+)kG&C*N31e~=vB)(0KP>)b`cQModl zMDDw0cnS%{vkCL>J=1l>hJ)D-V)2>}oc=np2eXD?m}ClMeICB;ZHDGZbuh{-+edE& zYxqdyV(`=i_J8k?_;(zwP-BfOy@OqOTeVHy-0QQnWw=mj5z^2W({d{xWXHs&q#JH+ z-257Q^N9GEdwX&c~r9+O*8Cvk`nA%rd7+ghI|naQ+K;I)&+I5e1` zR=2sn)_PMcTT-ufNBbB5T1V$sU7@w2XyB23l|_A2(@#9x3BUDsw#g|zbpP$|h2|Im zetMvfe-19pbNag?W@yY+un(G%MuJRE%bqs(`Gd%NaGF7}ub!}*fKkp0h0pu6?A_Uq z^!pM?3DNa;Qc<3#zu(lA`^J&Ib-O;0uh$L*3mAQnTBno>{cja)9JH}vWx+W^jU%ql zZ&^8A%ofGjvGC)b5~zNw5~Gf0F$|&^K+NTy%#p4eaCjgj3U%j|{xwcJE$QmKdrK}W zEu`<&C53-0#V-G%#NL3qxUA_5syJC$bw}x&*w<#i1xZUNu>MntJpN}z(B{@Dn?vF4 zI%~a~)9b5NZNMxS&gTMhd709vQO4~)t`z2oJ;}CI{NRRfQF+|Z;LtefJ8J_$9bt%k zhs52)^6=}&k);+MuCN0`Vl&pAYMderR1$@+GJ`pTWUWurZaZ63i=RZZj=+M;BU1}T8jreHO*7f6vBjx5g|DgK~P$B9qyuuHvkQ~V{v zb}GY`%d64RyV2jN5e_wkTf^a#rqbjV-Lsy>*}bRNMtoNaJ>$E-X?0TQS;4L!>=TBW zj)MHDT>r+}96(c6DeDW_(O<<>xoVN}3b#%T?rMi8WY}KKj@NL&L2U5$+}19QEy#EG zD5Z|wnvPEoCnjWW^$*Sb;!ZQ0!t(J)f*|(Zi~yN+jyle7Kf=7v1MA5=R5Jb)58A@0 zN-*zlx~{Z~k6$-hG|Qva-(~vL7g)Hem9zQ|m+c(fvz|h{dJBZLTVm_oC;O4l z0mG@bDNH$cPQw{xSPv|kd~F`S54%N^_vM&yE935`fhqvolnh4DVypqsYHMLiDQtPv zB$iOx^A(y3A5SV}Az@O+E#S{soPW1IGg_X(mkb&?*pMNx=5T7f99EGvEBk$2Lapo9 z2%nSoMfZEnnP4#Cl?DyWis9dx!Ai|drlAVDVAWv}BiCX{{!csQm8!3U*r$VY(%zr` z{S44j`OkJP1f2~;0?*&!kVq?t1%c)N)lh7wq3old^BY8hJ-JH;<=9laO!$LsnDvDn zp4}*T3OXuB?4XO~F-PcYpet$SKRI1-@|8$==#g3u-WbUc9?h;hYhItTt*&|W(dPHq zh%Ek>FW{t}bu+#dTbq5mXL9&4T~fB*9o`y?qkJOiB$LgbN(TeUWFsWT8s&>!g)OSbs^AO77YGX*4pqx! zwP_uOZL2YGAbTWg@sC(7D_4<~XdW1D3~(ra>I46cfG`5W^qlt_txkK8egikcN#M2r zFP`b%)w{t3S|?2GzP_3*1(^6t_)QS~?aVM`EJFK8rb-~$w4XCOT4W*{Qk-Tf{cm7s zZk1AkZU8+?Y(4Ar9wmW;@*birGZ(^_!k!01uV4PpNcsR75|IUGOAWNF4}AUy-#$^e zY>Lr(Qr;0dXuQDIFPM_tiU~4jpnXq3gzyv8xXkSE;~E3u$(O=bS&E}y9s45CMv$`x zN(}`TO^hC9anEV?eAeN{M6OT{1x_1mOQMF-Gwi{GOmtC`mBvj^a(7*P>VS>5RG-S`_o+ZtQC8h)!yLj_*{k`| z7_p_ha&)?keZRfLHE|e27%+%eG#rcozehuFm<7uQVXhXw4)x_S;hOCq>NIT8s9Xyh zX0V*^ElJrq-kHKc_^2pAytiREEt0-@a?G8*pbbynlz!07f{(||eqS*;#g1!s-xPodU#qx_aM&%st2M}`$zw!oAmPlmIT4ZCz;W}^ zZOpJ)aRN+AXaP&EZAg6Y=-Xz_eVU;~eIKQyY=>QkKo4AGgpX#z7)0_E4g^ax*0O?G zb9J^#9NdD_#0ZbAzRDeX>|~&307Dfc$UT2RN|b84Y;RDZ{B! z!QV31Wm!-G)+Yl+h%1XfBg0wkvc%ObX z17#tG3Ns#(dJ(Ow>2w=>K=5`T4^2TBbyMVd8M?d?L+t~e`k`$D40YbkIm7i5?X3T7 zQy*5pyQVNLg3w+)ZTc++;9u{t-ax;nsjQ}~7^T@~L{pwoz4#jF+5C=6F5FX8O)F_9 z!Ri(*j{#U|@nMCCS3;O3{G;@JdonH|!pVCI`KC{6x$(RokFh{6jgbsuwAXKvGbNYH zcue)NFSjiB+!QC}>6y6^z9f&k2ohpG>Zor0-sX$5E>{4mllRRj0o1NfCUNBi4jL0P zQHt%7Uvbjqco54h!er$E#A;ha0toGg{b>_#&F4e0eX0UMWFU$ryHEk0gAkhAM9y&_ z`y4;Q7nnU?wMMM5&8(~xZY|4>WY$aRiIDKJ*?8ja=mY(R-Ph8~)bQQ(un-;@L={;E`q zi)Vy@U>+%X<&*IScs!Xq`Zc!rWuJoJoa*#dux;SgWl|aRdaBBC8VuEI`;%`6=O#l_ z;`+Np4HY*v!_LMG<$OKfsL~Wg?C{+r<>*xgA9-#m^0>Q zMo7+1Ye+BvyDGEy;qxW`t|1N`+_=nZPEW!<70oVH;xB*b`UEpqGFX5ra4=tPj319i zScXr?BjUKpeyc+H%0dqD)l;1iq0q1?`BpInhv(mLa}7H>)aMNycJ{GiBtkWRcb*2> zd7nK9FX577k>|)yPOAG&AJsDe9>j__`~95)euvynbJ@|^cpSxbp!E&s+#hPvHB(UW ziNm5xsZA(qAK|Ac=&Z8RA0mtjAl=$QCXlMwpCUaZ%xXOu*{);_sqLKBB} zr4Jp-`s`f#hoQ1)rOrx%&Sc5$@VeoTR~lh% z?s#-Jab(ss&4?!IEqa30NlhlmitJ2O|cq!$*h*)|mcvmVfA9qj)atX~!WNgx7gFSBYYGwu zyGmr$la=o#ahxUqFUYds9-_U>v)S?Wc#G>KhG(DB0sjjXEgv4mB~QO*JyyU#g;}Ck zaf2L4_ue?{JM92#{9JD6?Fb#M+AXL_rE=u-7aDVZLGq~QEc-d_u6f<#lsp+zQFw}0 zR#I-VowX{zr{(o{T`K0_P3vTg`26Ll)YrSQ8S5O;GDcjgsR|V+D}QyhZ3h#SpjFV zxn>=4_-7eMxW*-1_lsE;Sl@~Zd@?!}*QJf~8eg(Nx{s0GFsA4umUmL7>GQH2DPO;f zB*P%s+_QhWq<;_jLdEzz{5QFx@?Ju2<|6^t-|hzWKCJqde>bNCV0U~;o_AG+fk07{CZaewVX1lcmWTK*Lq_SF&bF_{ z*7!)TL!x_6nL0aen0XEbO47f)HQwC$W?KGT;k&KvgX!K#vz=+wLAO*-I`Hxm<>vpe z0J9XpAx7XKUM2Lg7noi*Lk_$;p#~(8{)a!lb_kmW%Xh95ZD*C-il9py3}gxsA7McD z;bkMfZ^+Tpt-c!oGXjglf1yiSc>1u@c>$VLV^m0R;zihH$hCe}$b(ISyqQ)+L$I~t zO^Pq7sHr|mI4s|UP5bqd$UPmgF`pF=i!*Bs0HsM)7`AO z^A-9c--8QgKBo_dt(`2U+wtG7pnq;a(+!JWj$V&+J)sF?K;+(SnI6DPe3TJ1Cq^*< zAm|`Z-u7Qy=JKg8eCSMO(i>s*ATM$Njo4^ODT&*&B_%=js@pCF?x>br94ZpeagHm6 zGos5n&xgz#ha1)-hSSZUa>wtyIEC;zWThsoBX6@?bzydGL`vxlt9= zdh=6^7`itvwNG`Sn6fkto>rW^wt7WZ+~eqCVXVMB?Svj<%8;POIph3(X^QMqi()fw zdF*g>SjfsYHx_J;c+Who&%<9E+Tt}_mk$A=Z%CR=>cFq21S(yAG7kHF1b&Q#*RT5^ zOtfMrqMsUe-OQs%NCbBuPAchvmVoc9{1Ouyt{?HbGL^e#Z+y7lESdua$a)Qzdt_^) zGTOS~1x*b`IB&Ruu&Z<(-FSV|3&PBqbxMijeVxd3PiP*Ingpg7t~p$}IF2A#gVUM& zt+rZsvc+>>T+8t_8qu(LmMDH-yra(>yR#jiPZ0n4W$1gVwgOtHFG*vhsF49O9PI4H zedZoO@DYwjrE%B8gzmHluXIaauiDo)EhoqbxhKtT=scvqW?oCCx*HLq7YDTqx9fh& zps&%m+T%ZGIVIqHE$#dmp>=#?u9%9)IkL?y0<)YB%}IGk>X<|US#MCr6;Vxbh$Ms>O(v?wc z$$d&fs*)=Lb+cccl4c-hH*nkyJlBo)_YMRbrw)xSRXnz!T|Pyq(GiA=FK?pZ9`CPF zzxBzrgoP^ql!pa4T#`7)tDpxe3=hh26V1|RuJcsD?9>AEF^53o#v|buM$X_DaYzU! zLwIZM0d=Y9bsaw9KI;kgJWqP`Lj(C_1QfuP{pEwaGU`mjBa5ctp78*4{C$!dUNA0= zT9bW8AE%!BZvNblhM(y5S-boOGd*vSK(3I{MxFNqpWjte;PKUTS+kWCANCHO(og~? zn%HcK6kdnwDb-1U@)BNSvf44+;B;HRqiNVSv8CR5R1wKm$JJM<8seN{ zLLjW&SMDzD)4s$hMeo@0VCK`H9Ap6>Rcl+@@9M^0CYi1*X6Li=aa|`kOQ-QDa#@?^ zzM3Wqi9DFGM`csi@_gOSPfnt1ju<;WOisTI)tQ1IGt-hRAI@)$NMHFfD9*bec}c@r zwFV#E(|Jx==|fdFVnYkwq*E3Z@Q)-YoQL@%Pk(e;n&cQUy3;?$1S>qYAJcKgK>-f1uQ5gj!@G+Ub3=S z&%#bWKb{g6EZ`GY&Lbr{#$?BF4uPa$!5ACPcEVN}aBst;;MJK$*ES`&MU@-3?UbG2 z-7sBo%Di>~?6$OS)xzQSiuQjW-Pu^&$Xe=@9}*u{lzUH?0C3#V6j!W|sEB0W%CB>uhsBNDO_lqJxw+m>A?dOW6 zaMC1~14r~G5d>@^GM%4b?ZV&YEv*c)WvW6Pn67O;shjTv}2z}JW?c^?Hx>L~CHW4+Z3 z;YH3htbz0c-QxgH` z`e)CtZMj4#IJIOMp0>tCf4eLTF%TKtK~2!_ZIgR2&oez*VAxl&R<-cfSFWhd`amIZ z>_iyQ(s z%DjMkRV?rvpVSOS4adQ!9{AeM0rZmORsJmMrvF3@f6pEIJ7jZhr(fF8N$Dmjwep7+ zbsVK~X3E@HS;kF&r%(j}H9kyIimyl$6eniC{st^4TG`ygaW+t--S9*8%y7Q1nW8C>gx61`p&PD>8nqIs;P4$F4Uu-&Aji2TMRVg1Hh*aOI z^1wx>y&MZB5bK!Af@T-p^f4A93qnP!_qG0FL=UG#tz_&~lg+O0JTv!N%lT}kOLb*0 zb2RJ%&-~AypBvM!@gPJxyZHy8FMtIZei*z|#iW3X7=`4OzcoPpo_2%!dEB%^!`GAc$l(qz5eN8 z-JzMs;xH*8y8Pr1W~M`S@i=gdmpx`yRPi7n2(_qkrsbPYa;?@ISt;|!eL7# z(RB{f*r$`@xzwQjV@J9#?<*!TG2eW|{fZ*K4dq}0S`O#Y-q>qJ*SmWd?*fXgZDb$MeDBm&S9;YOI zNqacOYNVt_ZnqHqXl#pPT@-D|xZ-RDMZKu1L-+9^uDFK)sA}+I6*~nBct*?FfXxY? zhSY)bd^UAg{JY_%4-!qQMAS!LP$Z|ZmsdceLYRh0O`ACWTO4gwd= zv@-|J2IMnV3eO?1%Abb&QRxp8h`?yHPJ6Fh%*{3imIZ z49LmhCn}l9l0>$;Y#VCwsn?K6o!(a9YWjc7ef2|B-}5&rN-4naSukTczai3hLLR2K$J&C6ax`+g#sr+y*$8FSXvI%pOP7@)e;o^v`}k z+Le))57Fc&{He^K@pdv);c^Xl-+F8K41+I4Ti)dTr!83`Y{)c~qYC1!O<7gn&N1C{ zz*1Ib+;j~SS$E6~R&|(hVXSkVV11*)0aafu_;5nvKeP&LAUH2=^RN`hJwnwfreG3PzddiA?Equ5c#*(`D<@x32RgIRk@rG9MxDSQX*YkaLm zZ5ZyPoR8vS6hX1{@#iv%b0vE^G`z@G6{0z+Do>OCrgY6Yjk}MHpJs^InEL1B-8vnP zz8@`GKE}ETm3~PN%gGH4j*SZkKU|&vINL`2i@=(0)n9BEg_RAoM^iPL0QW-z&9eA0 zOG2)j4r`MuTD8*HN024KcOMtX1JV6Q53l`u8*np$5LgB;bTS;#7#aLr-Pw_ zADRfw+H&%-8JnwW_>K6jY1N>nckOY!xOP_>-U2_}<6-pML{Wim;GAS6LO`TWr?o z9#xt?516ByPUvqAPngMF{EbGaCDEm6WXD3vf{G5vf1*W&bMvNw#V=feenZ6*$r{4j z4NZIY@{2J24O}X#fY&3^5X;Ag#<_wbZgDDyx-JB(<%j=3VUBmsiVJgN=*1ef$~Oj1 zo&K$eLx;{<>CK?5!}pmyghiusCYZS*Zf<5#V(`60so})9?jYaPe`9JXILJZyqp6jX z#_CiJprwnx7QQ+5cZw3(w`(ds0kfbN88YA&U+m@NQW!i%7X)p8{CEhm#pu=B-j4`L z-o2@nUoiyJexvPJ5MfK8~I-dhLAU7 zX6B533&(;wc@M*`V!ic;q#R44f*nV{wT`$8@s?Y8*_2wpfAB?7332{@qvoMK^k|}= zYrv5 zopaIZ5OgvVm}sGAadG8w4;n)b>RHgnJ@q82=iZ7Dm{I%+ui79f#F(We#6@1UaSbYF zTN*ebd!bl)XfIa0@-P2n`)sA@DsrSRqhs+AO=>%Q(Qinhyi)O7AQ>(Ra|tFmhCcX^ z)znNG56tFd14M;m6TB$vV-d~Qx%zJOcFpGHj`@d!o&?|tpKg>mAlnkyHGb8G{(9%l z#&M=){z77;*bR+ZT9q7;K(F(HnE-6JcWs1Eszxb82OawKK0)ke(8KSwhdtS-Y?_aj zF)t6$;gx$fj>Z+^OZ3>Mm7+?^5F;-2y)jID!Tf?(u*BaF@vzXgayEWLr!VIjQt;PpB&>z>z#2R!0mTud_)lsZ1q3f85gLM*9O z5%R&+Jue<&pnN=Tl}_fyalGRH>bu3m_z5RVB+7hM^`^~jK2GVVQq(YR6b{R%2E1iR zNC!r}-XG2-o@&-Pu z<@KH*F7mDBx=Ag4p!DSjxBr^8ai=Lncw4WAsd5vS<0-1*C4a(5SsGBq-d^lM!dcoL z=O_7l9b7xx;Gy6=I;xt!1&xVz(e6vk7 zZFe4W_EdJ_*OS?CeH_kye_3e`g$?xCaLB-9u{&GY*p@> z%o8?8;wjzZ>{2Wn3=8vtnJ0LTfiW0y>`~h3^)S4fCp<$+_%p$9IL{uphp3$Zn6^Z- zBL5ngy)5V)+~t_e+@Qv<5xknGHL|+!uEuhC>{B%o*g;RB+(GpU6x`v-yYVX&Lrvl) zY()FX@R)`_GpUP0U(~DJH({sAlxl^pNJiZT&4z+IoHveM6VK35Yn^*w(wVOr`a#=J z*lvf%YaPjRh5+SW^EibevHq4>ve%2`^t)hiOSzJfp>YR?7#G+KvEf53aaNc!4N_=Z z3bI2^)sqz27i_l@BVarmZJLwOa51 zUA_8Vm6SHl!HEiI#I{=JKY>J^e~kM&n8>xL`Dbe=2Ld!ax!JAwhk?ub*G?+r5lzH= z#c#2I7;YlJM%9NkIjGHM*%IqCRg(L};Xh*tBc4rdGN2Ci_Vv{SFv2j6Yu1mm)`l|*b60f4#g9T>IKVdp93i$ ztA8XYpo!2`1~AcJBZ2a>D@b#a?IUqcn9z;#$erRFE-BnB&f1ekj|&k`-}(uuz~AAZ zX=rCF+Zey~-4>zt`lUseNYyx|o8>|7{yH%TPzeMAY3niM1$VOVjQ8|Y3Dj%2pSocR zZKxEp5SlZ=9;@#5t5po%-Fbwq&gl~NDe{3wYnOwgf z3xEVq~IBklwdS`>hSxK0SD>tN@yQ${{w&FJ(Hq zc{QD*G@8ngbd$-l8BOyYw?j^jj`ZJOova2|onb?NZy5rl!(!Zf7<4R>wN)Nsecp+R zCzPROc_q53D;cqDyvZmQF9j24Ea9qy6-9;4J^1G~yE5 z83mEtAfxx#)=CVJD!*(z4X%9_iVa8_EGF5U2Ck-ad8Y=%9%soFo+0uqNkEG9cN}L{ zPmoeRwmn!IDGbQ(nW3a1hJ;F#;acJeG3dzTAkuFL=QXB>Xmb%e*GFzs${OMZQ~cV0 zo$yN~770hDNwO@qmGo2`NTS}BKixjG_p7r#g{3^og$%x`?y3~;4elZmSM(u^!-w4_ zG8Os>KyQAA-MYw8z=tGNOcRSeq^}$J5Z53D?N3?HN~SU|lky;=YeO~HD$M|Cfl-r= z?>>qedbIewgSJvI=3Jw*e}DZ>FQ{|RwYYhH8r+jV2CA)dp;j*De>853c*F>RlF z=ywkviQ%;1quG}$6ZFg9OeI&=)*249CR{t;5lUU;j7#%U!&IsXRfePjvpMA{%a)3^ z<1!vYx*VvnfNaYq*r6R6>{I)jcTg;;OApXaG2Mj5tBGRPZ?SJ=Bn!zu`Y6k(E~hH& zD^hADPkW(uYV7$%Qboze&(}4ie+f((Kt-0>RF)uPmJ&z4(eYd~NP19yW@GIbgXje* zJ}tsq)PoVpzFD#J%c*I(;vw3HCjoiPenom0UeYYVHwY^|japS!s*JHRcin^bmq(0a z$<Qq-HDHMa)jY+ul}Vs=+ipG<{OH61%e^n)*Hc-4skL@pQ7SAT5{jYzI$Xl>!ot9_ zWv>>)Kxp!+zj83Hj0io7*4;s;ci}un%E^&w+!v34?=|hkP@1&6B}HL*dHLX&FO^+Z z{&x!#477i?`Fk|50`|(4_4?GEA9bGAX0DtIvQcu&<6-PmIhWU*TF1fOI$-&Y3Tf!NZ+<=By=v_NQEC9vKFtq zJZ;3$5r@v-A6$j=JLa@3hv6>vlq;bgbO-4Y5W=Z<4AwSm^WF>7kyjGqAS&u7170l# z^88u<)>1&n@GnUsCMU;l#-Ron$j`bx-NIUmbS$q%8*K(2fbOIJ1uKI40qa$<9TcNAw}r;hcG|XKf)|tAYy&m{Kz}d&VWPEXENb&qB4<8)vwzs? z5YC1$FUiuqYy|5=>-*IXzBWkz$RZEX#Db^_45=c1ZBME|+2TR)AF%L4?4r)Yh!%Y- z7s?2FzaM_36ptc?`=R~E_!}O^5lin$BIL;cKpS>BIQN=J=Gu>N?1EP z8X!Grw09B*j338GV?Zp_+b5*%{$*hSiyvm5{O_g0^C^j(%71@!bgZiDbEF;3R2p?_ zL(9X;UZe|98sq%Gmp}{&D(HEkJ}rJ18;BEr7=cjY$!T8xUwVhVK8^9m$d@?JW^t-w zW#dVGcKrA=zyWrDtk`QWQUC(@GX-k?>-#?)0C_wmVU#Qto32F_x2mFZ_*+8d-BylD zB@Nzv)D8GMs%;WNq~hGJ21o;wy{E1Rza{AM?VZKp%g-2My_3F){;x~oE{e26do=xr zFlyMRSTni`(C&PoMYwN%*LTCyrF=pV#+hCBzX>~Co-_m=1xUMY(F-BgqiUw&m-}u! z%zP2H?|1$LP0=s?`5Z|#+p&=PVirUTfuaTWvBER$A4Wvr*_>S3dk!%s69p+3%Zyjy$o1Zz=O}dgp4vkT4M>OL(o` zxP)x>|2Kkx?XKffY`Dr}2vif5hz-}r#yVVmzOk~Av#soB!2mH<&JxJ0J;@#O|1Ws~ z+WxcdVm@SvqzYraaZXl=PQ0Uw9C9fHJL2t|f{CjZA`WewbI;c;vyQbGZZy@ys2nzX z^#AqkRe&_-2hq<&F7oc zNL4CWB&}oSm=w~2A+FnI&px9F@;(;v2#ll>eGP_zgLp}bBqeaf9Kf3-6PJ>)=_$&> zW5jZN$84CP`A)6+dXack_1r{U44rbh1|< zOCLI^CIv&<1}tyqx!%7`x{wtEtWqKs-_vi@RQ(TIYM&eSg2XnB2)S7DjTd1(^*_et zx|k|AV^gFWW+RoTGo9duM5Tqn_v*<%CjVXOuEOY+mGW=iLQ1Va8vT{5lva6yd_^sJ z--I0um>PcPz^M3xL~e&VQjdu~cKty_R(RzkAQE{0!W_@%i5flqFb1!KToM?yN5}o2 z-U|f!&J7OPrPG> zP5MmYh=Zr@M^pZ(>yTj4rFZ9_ZyMPlr03?)!yZ~|f>aR%)kDGfd8%R{K zcrrOU{F&_Wu`qDjtg`yXLCCDUhV0D^TrI&g|$s z&Zv~taHr3*^7n>@fuP_KnCk^iERNn4=9WZ~xf%)Ni9>zyE^buvVS8g4E5_?1hng1z zeHoQ*p0)$(`n5XAOKVieAP+(d~W6I z*={8po9s^$CU40%fBk+^Zg(|veRffF@x8d=T_?mT)cI4cG``9J=EcMY`bzhl_1l#= zq2s#Bn5Hc!!jjz$3|mWXNI@)|u!%+H zKg`yDdU+USlJ?=$g|icrM%n$G{^N-!six6u!I~!gBjvgIU{bqbDV#kEN#YIsJQ}ol{O$S*Ro_i%PT@9`| z)Xd(8Vw zGiZkhQ4(27Jo%2zmY2av?MQj1&sC73a*Ms~UG6swTfqRuwN@0tfjfUbUIo0_~$UNV>;*FvOcU#Su)ZK z2(8%{MFQ7PK&S7+j6)vFR^^(YXJf23OO$)cIB~1;mZ0u;y`~K1ILBYzG5e^6P@lT| z-+v2WkV4MfabmE!KdFb|obXQtWupSaHD!j&CnSRNgTReC{5%?b$~`7AoG860sr`<- z6MqoHxl{q^X`6phQ-bn?-+Kq77SzQuad{zPK&*Q@sutWsWL(;y2!EbpGpMsQ?LV@u|)yk;5RG763v9hY0uc0r#dTnmZ0= zHBq?Xi=r1#g*DXIh(!I@Qbz!=il4o9#SnLGPv>Cv8Undj02v}#@+6%#oWp zhIse)RPq07;~lNV;W_>uivH~1@K0B=@!g{T>QRW0Hk*Jrpka9|hk1}qaNg9A{x_%b zo%m#uhr^2Bi7={0h;J=yisSRJ;q71y0vehzCJ@7dfQ4~rw}~qkOd$hyqVJ{nXZS;S z38Dkbukj%kf%WXt+MguYB00C|bFBD#=a{~mo+sg)mcC@;y9<|1rbX`tGenm9enzz* z5zdp51lB`_TfWXd(rMml{|)9hLgtWArxM694*hl&pH^d3&VdCy`_Bi8qlV>44|BYR zjIZQMILdxgy0J2wqPri^zb}s>l+M`_tSivL)8CCBD@P(aEkC!Ri!wPVIXI|v^R-nm%M1)qaMbv-<{9rnl z8HpSs$XcGHV?o#0*6s(D=Bgci?N^FtHZE6enUuJVJJfv*1iFn!dJ1LofXAvqwkF*I z$$eM}9yQ!K?ev9cjb!8A)WoYFeTA2DXEu&kTU$b6JB~*!zIH!!Ir)QJZ^h*RXt(7~ z-!<43|B8K7}}AqUJ-?GXglpmGy;4cyNT@{p1vRNXt}j@$Q7d99@U!eY3w!7+cLg zhuNmW4r!5Ubnz=}|9zCFfC|*%HPekK0j#ZN_eb@40p%fcz;e1YBu^63$FB2VG7}Lq zI#g6u;{)R<;ogQpjqQqR05S72UIaypv}J%O(}6YpeAE@s2rHyzK{y9=WK9=u{!sU{ z<|_>vpUL~w{B3+|)@W||j9AgBD$7T7M?i49&R5>cmlB5C=$cabDon@LbpBMDHqC8K z+olihI_(_$9kHdQw77d}xoD%H@AoQRBrOTnz={=g?;`Gh4v;2+#$R@SH^na%(<$gm z-e?^CUG!mUL66GRjxIVhSj(pPA&Px3RePW23DN>uIJ-T3bh6Ei=o@DcN=i_8iFEeb zovOOiHn6F^c)F3F4F`Dplikdw3_!vESH6FBX7 zY4N3X=d0@i5?J1WU7KWTQmkH-Me+BJ?$wlAQB_|?&C8WYsr1>=I*Xbu07_jSA!nxuSg7|?~pIwT)=A_r}b+< z@ll|SU@_&_=^JCA=#z&bcsp`ZNYWu1r={ch+x1NVq9gO=tTLl;0_<>L8Y!mX(70n% z!$y*AMW6oK>>{{ve1%tRqSoO2GnWEjmd-&8T8L|+)xB1wt0S?skq7%p5_Ac-$=Ik1 zGdHayFlE_@4;uzD>19i!-}HQeZpDC&-w4WbPRE4jD<5Al_k7`#>#zV5vACqv*#)l* zBb6Cr-%K+NhB)iRjp^OFj~*mgKx6xpqgq|Go^dkbn^9Wv%j}b|IZioX&=iXNECsty zYthObQ{=V@pq1eP7U>*o7@Dv?W<=J8Q_}RF)yVNKb!)!#AF4(+lWi9XRL=2PuO1yg1tWDxL^O)1`t{p(t>Gb}*wx)3i`}qQs#kMrdEv57hy3y{}pp50<`x zt-VT^wdJUQYQS7c&><8tJp_}q)Cxa3XJT^=3REJqn$8v7Y#aCN9J~9yC05LNMD1Rz zcY(RJwyKD(4h`Y!LMnlLv74u&;v8Cq?iYaR?#hQ_pw zAxz$LmuAgtfUG2$Rf6m}?_Ic(thc)c4_huDnc&ohOUohq(N0Mq!bLu>dy;!6hc}ex zhuEPNr}0~NIzBz~bEejB7MSBhBvoQrUoLk&kE>9Err^N;qKD!SUO1@}d%RP)eIY9} zu>F_Mc@pBQCfo|9So_W=-$pOqJ-<08)%$<95Xg(_eqYCM0@ZIWSArc2%pBb_sB``q2T3%)pwHz~rCfX>1EH9Q^Xd zO4hQ#k{-sO?^6f@wUdo2wk4l@V&P|mm^qG8fu zuoAK$e4+>zK|D6DC(w&ALjve-Xmpn+X#-X} zcAYO~G<7?W^|x#&t0+AlJvwqKsi3G^V)<$`eO=a2ik0;(GR~Q?Qh2s5E(9N*;`VrdC`89gBXlbm7=G0?3$DT3K ze^uKJCL4?(M~xd4t7ppP8QhOiWUE6ndyJ+F8aveO?B;alAd*qb<~%UgIVE9R>!)lU zK-&0Q0UFWnb#nWGB(wlNY{e7z&9Xc8)LV^KB8Y0)Cz6m>-BY(ITn2hyrv;4s zEpybnog(c!L}73qB`$s3mK99nJba%4NV5|12c4v!$+~!9JJBZEuslxY2pt+OR6~MA z$I4Nq+z&wcTBG>RZO^UeHVV}t>9(&5f(M&Ikg0+@t}0hEEv*HF#=42S1G^#IHI5=Q z;%ytW@H+ZWB?SBq2~IYp-F{ra$sZamwIvqX)ws0j?MvO2vNsc5E8a~{fAcT^pqg}5 zcnECq)@Yo=5+57GtliPUm|XwXE?Hu7lv+^Zl^-fwTxK9A;lT?G^CkRJnaYl4!CfV7|9 z6B)+}SzJ1b>PA_7CAHe-+JZ*wkon)HdL(Ti{mi^cR3exs?!4FAWHP+tEexCD@1Lw% z6FR3gnzY0CviST<_MKtX6o7TYLGCN!*r3~J-g?VqnXwi}r}q2bc#`5+7civ(sP3@r zJ+t3%Vzuh&a4BI9Ajkd@?%?b!s(v)cgms;ExQ#5-*~YK4JXpG$dzfx!H_Gdgba1hT z=SKcUt8rs#tA^%KGa~%+PPy>EE`~WyG00d|sdd9&E&6*;oZ=mo{?9n=9}JthH=d)j!yGipH2?z(jq@F z?M5?bCz?Y#UQKQ)d-;T$=;Ol~h!)aZ-Swm^OSc;8*xul1tnU!u74==&B#GXd8}ilX zvi7RPx~`e>HOLA!7?r7C4kd{Zu!@|n`ySWgQ#(AO!7nJ#OF>LvVzGc*nD1l;wVIfy zoz^#?(`H%Tr#MC+{Y?s(M18b`6Fe;T5iaYIQQ!X1@q5L3MaDKXgq5a`G?Im5!@v>L zwpwn;$g$=*Cj6w$xZEZ0@fMS76j`I$d@mkr&-g_+8??e{s@MVp&EID5Sy@;LcWX32 zh)k%6eN7kaN-*Re5uTHSNE+ztkr^#d19RRz`$hSyo2r~r2|4vj13ccU(QXFUv`SKOwv|c?Y z=nwojR_z6aTkj?*O_FaZeAKpj#+Bko%D=cTIb7Vwo)SG8?G|ZI+Vnin`ax4b{P|>f z8V^!g65#UnP1*fUrt@1?cYt^h+^kr)x%fC-+U$%O;=(tOHG#Zw3MHQ-C@m*)>9`1J z(B8qCJMyhJ-T{T|wGIei!eYuibSRk;`oKMiz-;=2!{iu%vSR!7|5rp_j5%(3G@|b-9TsDCW8OEz?eLeEh8XnPQVv1* z{m-(}yh$-5{_<_ti#L(|**$Fe`|IS1s2@JqJ$m|z1en@;VJLETp9pwg(AEBr9{eIu z@iZn!5JJTE9I`_dW0RA_ui~o2UNSY46IS0?RVI)ESJg2kQ>k$l>70(hZ6s`$92qF( zFdd=E1kq=EP@ra1BN=yOs+ewas|)`L%)9NsWF;B2c;zt&%9dp8h?=kt!h*2eX`7er zA3RM=t6RT}j3!G31FS|bf|nh>_*S1_l(Sf3xRXHP>WYou1$ETow_%W~2}paU>nBoB z0t2_A=z*hG{5D%xAMz_(bEN2o&82m47mX70l9g?XrpNRlo6Nqs?h`D1zm&5Y_A;}D zf6htr{;h7b*dc-E|U9Xq>u=I9)WT&Dk8dvC2#%_6(^KR7gWfq#1uh);tEg`ZZr?!Mcc?zBpe4FX{zptj~ zEUYkOUf^$BfE!hc7sZ5O?Ht#2p}n#OX*1JJ=g`RUo#hjvTgnIyUB|Sbi$C7vpU5Np zqUhJFI$!(g#zDw^z{Dx$w8fCCCQMwGE0&2s= z)Q5>p0g8l%V2*tHK;nD#;|m+dK?Qsyx>cmH=mmym2?0r7Wm#)T5sMX#B#;W%9!wW0-QT?6nC=fDG5QaNlSbx@Z~R)4GTTo!?xyX_5@3(CQr0__KCaY{nseN z#QKSY`P)Y0O|h^#1++LFX{OW44cP*UsbrA#8akvX4e9}*$%^T< zCBsIcWL`sR163Ip&=we#TNhl(#JP_^@ z4N!%^#EPxw!;i(p=oF$jSiz0<2l)bP>7K^*t`TzN^ohJaNFb%xz1s#Y9L9m`ofNuS z_-%AJ9rwW~FK0cxY;`T@iv?J2+o^55^T>y*c_}FG+4DX~QPTmE`Kwh(m@wQ89Jq<_7G>m@BqfrlYLcEGbZ;Dr8ihl0=K zKLWz+o^PoYDawS%3==9aSStgsBnEL@F6C(V%Va`IW>E?#D9h@AeFpB zuFvM{u;R`S@ioOEAFn%cKxx@8PCTIz{+%BCYQDksqj^+pD9>L6+UF$W9q~p!_;aX- zv-397ZFR^Fo$_=;T*d+NHYAo=yfqmUr|Rl`>H!iw9O+{Lrg-&tsTy~Mz7KZ~PM=K8 zj^^8we3g3Gvi96goDm2|JZ)+Y&${OCt6oW3R{s!wUAv#0G8GORrwIV}ifHF1p2U#) z8f`hii^qyyf6vjenXOyBJktFmlQWIg6t!sp!r!ioG0!^f4MZ3dMIhI<5xFx3BR7yc zEcplcwaZxBcPNz+^~%=m6Wq4RfHis(K@ZqyY8q#8mZCc;ZExEqqXuTlv9oCfAA9rn zz0?^HtYNnFQ-N8JjrFYJbyjhd1D?-gh7K|GD>1ov34csqRD*sh8yz@7sS&$tq_xu= zWO6RG)9d)+2YTG+zCq15Mh-8nVnA*u#ZzKBAVL4LLB(uY-1^#-ifx*-c;acX3`FE< zoNBTuN_$K;4=2w4KPI@o_T9Bd9fa&2J|=goLG%QpHnk;lGh-QzYn(qOtq+>{&V`$9 zhh282cUDvQ2{2K4chs}vM>25TVBiUSSODuBvqwf&O}n4^5pX&Gz?TF(2J;pg2@V#e z1Mrk9t3@eslg36=oU}^6UKpjH*;HR>#hwkEjg(*2jNX2)%6zpPDpHtBqfmMJw(@s5 z?&qqmkoFs1I#VkoqNBJbCsB)`;^IxoI|LH17Y3np*wD7+kyg0R3j9IF*3SS&U1l*q z=knQHgi|!EW^N{iKjXo`>Rdy)UGIr$Jv&#G9t3w+XUFw@Wmh?Fx4YgJEoyOmviNw_ zedcn0WC;lRl=8uJrscQuU0nOHDpE|}DvkEsyWBp9OuIfuEUf6~H4 zh91dg8;}K8$IA>7J}}1uX!YMmvElQs?^L6lxI$TqNo^F@f*^r2~ zMB#7E0rsoEy#h6#K-)Z|*9i{9X5#Gk+Xu_ZArui}i^yt$MbL3TTMgP1qXlY{eN2>y zJ(xqJGulkcolg@_A0)lxa&dB#OIPe`f7QCNWF9*%5!^xf(aC)3hT)Tgm17&b-K)@? zqxwWEZD3aCde?u-?=9&cpAHyu!_Veh_23kCDVVamPdUA>a@X(=r(Aj^i^7Ziue^CN zth;%KL5tK5Ijmn6i3n#~eZ?<)6&*yAbfr4CLPaaxxUa7jpYrl?s`e_Jvgx@8=Y9fN z>VD1``xk~RpZ=mG%x9n@fDlBjE4DYSA+l%TT8}Q>gfu(0U7cV#B=*3E z58Hy?_=a&)f z`)EF9{BePGngopU(N23v4a#NLPnco>-m?qm2icy;u<+#QtR1`-fc$reg0H#Mh2}<@ z1ibBPB4RGT$$+E$d>4z9$Ed@l~X}z zg-z@HUFpHc?eq%bAXoE8m;osXy{SFvag6MX8I6@5YOtz+rcT!#-d>x20R&1#OA zh`6c8ngxTR0G{9Zg@?Bgb>Za-8`eFM*1qZbCwxfr>B62E#l^f9LfU5R%W#?#=r;)A`4n4urE%8rTR;?w>Cbck_ zuw>~dk;^U?mnt<1&8WjluBb!#C4qc5aI^65D6u0%W`>4M^R1EWd`q3Qj4`Q9^e&y{ zw5=8wjhOv8-9^NcJ=PgC(J4xLsm0Ou!9D7BvWPsfzv!yy?NDv=s12A!A#0i6fdU0N*mZ6-nT8S;OBdF+NbkW2-5ZrZ1(5O zK1$Nj4pHydL_(%J60$6j)yDRFT`l zR5Ti6j2Dum&E%)eb94?{S3PjF0DK|_q}_v|G8{0r{LMMGevG68qD3)}jX)AZUaW}Z(=aW_e z3G-gYNzIDL1e<)Ig8ZI(Q@MF*F=9mtdOK$CAjZIkWQ1+$ z^r4FEo2558@eWQ?M@)wJ+QNb7nBe+Q-e!}7m%5F(?!pnxsQoG5qx;V9bqMr6%^0d5VQP4mk1e zGxO4G)t6~dbNd*3dz-T^$GqwUq+%R2qR_qB0K8AxqSu&3ARZTO-(}-dETRrAg4598 z26SY2-og69@1%nl`*GU8%oKm)Cm0j}rp1o7biZ!+I!x+^IBcWf;0~&91#teOr;*Y@ z#P^#jXRNE_ig?`6QRu5HK+z!AvUctxdV9?bp1h;&hmf(-*FW|p#z&s1!F;dbQh;#^ zdoTc4kQqJHR6b%+9M=AW&PgH{5f+lkIjxlZ_E>oJ_mDmemkb;SSyu1~?hg!MhT`-h z%HL0{rNy|LxN5alTADDWuMAIuJh?73KV^S$M+eP^rkOP#gq+7?5Kr@Q<1`WyweHO#*;&o|` zqMw&i^xJpavo^}pO%I>^dn)oc>Jd$Yi+`0m1w=)B`|hqqO8+{!K*G>x!1>eGl#KSY z1EdPCWd+zBV>BVo9})i9fVA99Qq|9**hqh2`LL%V!EgLnVSFx^Aqs83_R0k$n8|{Y z>87wC$8l31q2d}FH;Q9>XPq$9j5`N;uW2Gjbh|rq+|xHn4Z^CQ;z;>-6HppAOe7vg zy>he0g>lHFXRCja4`gx~GXJH0g6+k|MM=M`-5qyNb^5`*g_r0;j z#F&n%)Hl?-{?0zPvN*dl^+3$vDSY*Gx-5Oioiv3yEvK)hge+TX^7`Y^_1Sc1mGxr5 zJ*M|xxg2ox6ty(6e7WIi@1iFnx}%&5jSxh1xbv8W?2UMaoCz+!>=buANM(1O-*e5L z&X+T*wU}&Y(M{}d5I=J8h=#DDt#Xb^V@U3Tyj-9F8fY1p+8A56|{ z!9^8#7A+ZyK2z&d-zPO#5hk(Ih($o)Swb@Nt}eUo6RuVPCTBL&_n_SUI4lFd>~pYB zOD9cxzI6_z5kNWB&f>0ZlRE2d-LxXtIn@YiR+~ILV^8%!%FB2x?qqSrb}*pO=ka)x z#1%r7x+Smi>)swKUItid>&xXYKM>I(kX_!Y24E=qlrj-vgH z@1*r^V`uN-l3J7K`V!1x&gN>RYvS_`*8q`$?UwB84AXYpxiLFY9gH3;m}bpGADj2SmUS}U0VJ}Tt$}-b?^jQR(+~X4sI*6vC5T@lx0$mdXJH+ zW-w&GQT&NJyH2Kxx8x&m{`U^#01Hja)-)1m+9Oo|w5BC^&N+Af4SZ%70h~NVQNmAI z5NaOF1%6(%7rt9=;AszVuyybe?(3$fg^ujnVnO`%SpIDX9xJ=o8mBD_BL2Prr0D7= z#W9c>ui`TIpMsn8pKbds#nV^wimVqmTeqhpPP4ZK2j}k9b|c+RY4fz;tV0b`SI>`6 zT#ru#BReYc8XMlbPsKSgxJ1lTgHM@^P)!_rgEv*Fxb>5JDfkTT)0|)k0`+**`9a-N zz^fIszyCyxCR!;%Uwsc8=ebD*7bU zFE>hlTV$8yecRd`IP5B86&HD{yTv?8vt48EW&b@+7bH;K-(??;o*hwW@7ZtGv8v!z zE2l$k*4>5gcCTqMvdBp?N923S7wY-k&eA=(`LYGB2ueM4dbOD}>#=Ww%XG4b*Ect- zCqt|4cxHbP#eGDK#x^dAzTz=rNOs0pSY#jH^8>*;P0*+g0!FTeP0oDdgy(wO8hgpw zaO~Ns?KdI|Vx^ zuhc0hUE1UuW3sb`w<-KkyLl69>P@kp6bp*Qh1%~TA)Q@s&-Am`dVVc#ZiFVWVV17l zRcw{7stAqTE4vEydzv_!f>2X`OpAZiDri_;$TwQ(f%XD5cw7wO_J1_Bo?*DKV{N>J zX0P@TEl+vw#p&Pmd<8}NPeFW;Y^@YuJ$~{L{!v{cb$3`2_w$mQQawpb^_S*GQkUG- zeY2rv6Z6hvp$Xr4AHUd1YZQU8^yaDNN4pu^;Sg|US=<|jZu(NU_x+Q!H>olwBXI}w zY8tGohJMuDYn7>|#ja62Aemip%gfk>@9c@r_!j{hu!#aL7s1%NB#Hk2j=y{MUkStLYXgvSPZYAUu-p0 zU_^$O?cUthM?9C5IG2}np~ODy^~~y_B`&eFy-_}0CubG0oQ5~gK7iSnY8$*iP=}p; zq3N0$scWW@xg8Xj5`Wex#Y$}{X`o1=P<_)hqW0cqwXM?cq`JJPC(JjJ%H@RcWm{#+ z!E;3e7L7b&&H8bzfwJ8OyUn)7GCy0@Rg1;LFOa(cT-}|v$e>|q?V3NxH-Xi6N1d;eK5sQxI<#F=o+Umg`~A*`{fNxe))*xG-P7eyK09LXP#Y`U6dMn- zJhS*=p1`EM`b2&02>+6GB2TsGP`RU8)$*oqxPH zX}7U{|Hs^4hDG&#apUk13WySefJhDuA{`1yBRK;Sf`H^mOG|gCbPfYZgCHT@LxV~= zbazQgch~>$_4mK8=f!h9Z=d_T*fZzsv-9k|_S$QI)>@spWxHo{%j1=m-Q;W1Rb8eB zMe8|RjuWLb)|AUrm5Smcri(5WqWjAZ%|Ki=;%3;aUP-7h)GbV;8u(^(m=nvmPwEibxcsOu9hA`J$ZLD2kX=hhLPx3mOg{3cVcte41L8&WA^zJCRd z)aUQszVM@iAjGQJ)K}lGPO_@pcxAic4P1);U3x{Ezszm!r=qkNDb9KarU_lKbit zG#~Z^^}*^kW?tP3nygIE%B&w|T{KsFRrDw%dAhgTjVM!z>CAA1u0N%ugpvTW*0 zQa96|(c73Gc&9LYWWp|N&CB1Z2$5Bam6!Q+<>h}J?5Dy5Mhw`>ABIGIK=>SqeE2*S zh6$EI0cV0hab!-=u;tArAOHR6>PPTL(eF2rzy1|QLtmCW?`!jmVlVoH3jW=i8!Gwf zzYhP=?hREh%S=7}og9%>H3aK?I~w`Fhydu?Nx^q|_vYxg%&*}EoVI}S0dPL#N}SSm zw#n8lZ9;rWcQ3tCgJKu}rvTM9e*_0CtO*zr(z~+===b`=-UvtfU;MW~PK`v9FlP8I z5%)h~n!-%{Hz0tPK%Q9tVcNMAmogO507xuAWC_F>eK&($!02lbs&YB`hT>Lk4-~2= zulE4HMur~0YNAZ8H0_*JB>(eYn|}yIw6ehyBnJogPOj$Qd~{`B3;%N?fi7aQWy`cG zlLbmuZ8wA#?{MFO1%UH0a+C?|q6RM~^Gb!G$oJp4`?2~6ZlP;U%5X*&TXt09WC9#w z1J}EHamG8);x-C$s4o^7Bdhh`7a15|&cF6P@ov?qEKp0#yn_wx%Qg7sW-pw1p9zEi zulw!>Ko9hNEy|AAi0WtT+XR_3pr-eu0dWR!mCDj%WaNLT$|y{xD>hPN=n*UsR3>b- z%2DukpuRJf1M6C|Y7O=(xqn0!@}N{l4eByo^8ZySS)B1qhma3FIr-sixMpQPF1P=@ z1>GlfT^(eBIH1~(lUr~=LrufP{~Mj9`tJV%YHSZNu83I5kpWT;mQOvO1@%I#nF&q?<}_a8$r4M-CS z5>yWT+R~mH1k=IXk?S4mENA414(&HDko;FgPpO5R?Kt$OtcEe;0$%D3arcF#bhk3( zC4&@-OP>ZZP|G6{d^|-u4??rB#@^PF`%P$ioJ+M?C#a zQlnv{nA5hm)U15rxVIfBu)-uRr3Q;Cs+Y=)tVrSHewyETaCv&E=q2T!1&bG0G+mjA zAg&hVZjys(QiVK>4o7sq4-ZI|`QM-)^@pAYlcGeZXSI7Ze~){N$emGQiLqy_tl-^SE^pmY!AS6ijaam)69GZjt*r5?|l#OyQA{}R2Pkba{xlAlJafm5W|d# zSu_{!tQuLIbHFcRuXXcX=)E+yF`I0WTgab`fkQ7PZh_MvT`;P9tTGwh+~uF8?1)yj z4@!MlHI~VwiKD#oTmvj$rsBG%x^~^r<2n>KCl3>*o!efP(vbElKKT5K##yu*BBr;k zybwCMVS;&y8gBSVYbWDxU zGZ$LZK~q~-#pO}lG>aT;3sR4r*`QjC(5f*1-)p_v4-**pXAVPz>2mlbyuY`sn(LE; zsqYM8M11=dH(xWIIea7S$_m8&PYna;{}83|az~h+lBc^XJWl35s?p;im>nOFETVsw zy*o_0oO$Q7Z&tl0J7FDFN&daMT_yk4wAFDB9Lrx2m$N*PWiH$D9nXvM?TaQQA~J3` z)FK@pbE(fcO8bpA3O52|H^ zbttuYJUtF1Ma#T1^=VV=(u zb23ZSVu8H6;eDb+pyZfuV)|`W%7F@Ek!*&T*mCzgq;pVS*B!S18#U@$tYS%=8)f7q z4g1AE+@&yi&=D}&ktpLHa43U>HX^SVBVtFnV%I0wrJRE{0)XU{*P9F z0@1*JW)_&2j>F&5(pJt5n(Nr0iG35HLS}-DP)O4dw|dBAuTE_wA#%8@2@+qP{rx`= z9)6sJA&=nN9AQX*ww>q38-UT#+GD;`C{NIU1n zm9C1ADbS`NE;=c&!=ER3TBnSkD?JMgi=XT0X)Bjo(Xk5iT29LO`iEyi%)rg}Ycg%6jY{+eb!B;LBn8#vpo)NM-;A{4T{@_DrcKg8=@foc!2?MaJxX8TSH z9<1<%ng74+rNxb|EAk|Zw|1UF5n-B>r1V3fp%@x)bKK5?r$cqto=pK)c>&Su2LMJ+ zgdYlzDfUnk*-19GaJ{R|hNJQ%sBG)kpaJ3;BqZKE%GqGH_+)*?XI#Eq%D+`b4r=-z zNklV4Ena(S11F3b4vkF@Q7>|!R$FKTDJY9JkG~)y9PgE0hHk$pJG`$`daEHas0VPc zyME9H|7GgG7XA!EwH`*SSJXTebpg}tWrW85;{uNT0gH3Iq$uCXk`m?%{gIlhGokv2 z;OI{Wc^hoBI8eS9GeO+b*G#5X<(ToWu$c=~*Wg(Y^+-sK_n5{J1)`R-e|8iw?3y4)zCZ^Xfa-Ner}2PY!) zkSR~(kAnVc&8_o`g8>KB0-tVy1)v}3@GMnr)(G?$t*UyG4x~Q70i4@mM5H2P@&`#; zO6xb@W>1zX)2BTC&Hx>0%R1%??)&LF@mQjlbzOC*`oXMT3dskV*5E;U89tx0G%;-%#u(D(%*>h@+pP$;0V;J$A@6U#Z&b>po?TCO z2XbmKWY$mL<%w#Hlz{OsZBj|4X4A7d@AJ`^GTPrkjR~ScBQ*KZs%S4 zE#Ir$V7sCJ0$|hm&<;a^{`$9?Ie--=|JFCrS$Sbdn)J7OD<7x)gR)5eFbNPnZ6z!4 z@%%C^F*3+-w#{xrzERl>So}W?=7bFJLje@vZ66Zf!~A><|01p>vyWgB+Om@>ACiAV zfSH&&5M=&05PcM3MCfOkGpAa%oQ!97`YaP^$AXa8Tgon{FaY}vxM=J@1!P%3MOZW^ zeFnd9GQw-Qu;S~)8`TD9HxEd4rHm$#rEA=`uUr-*U&%@Jj_Sd>bM&`nvDWp)`L@fL z#pp9sME;8}M225srJ;P{=5IBWHj(LaJpLcIK1YUX`RT-1y9QIxzGyxeeZ{JTx@Bt%aC^H}at@`YTMF`gtP}4yHvV#=m)?%X zc=AK_9)bLUc));fYdh?^hJ?U*3*oD!=O@E#o$Vxx`yVgrGXqnjWTXo4&nhZ0VxQFL z)J%&my|e3>-cVkfqU_DAN%Y0k-G=S~V{vtmhJ&;J@_-#)iRo*-f(~`PRM}!84KHV) ziFWE>W9CFkMu^2mn2CTS*hO~f7l=b`SbRqHIXBVMc6*eHa_y#a;5k_LGsosn@mi(_ z8dFmcg))K5%l?-J!#{6k0_nH!``S@xn5)dhU7iD0n{>Tet;@4d;u~2piCINEO-%B> zU2|Uf7OR6Gj;l63W!@vJ4U>$~$@FA<4ZIl6PoKwTB@%+k5wKB&oq7o(v;jYmzBPyS zZUu?{2g{#!41nYd4y%kaF$f0QYIgz4v7A);HHt-#_12IJTFrdOK9}e97H|Mx9xuXe z=Khm={Rq-2^39IsdbTA=Tv!eX^yDlvG;i`uP%)Pe^=xc~{dcMS#Y4y+pgul9jtqu`^B<83rWdD_>c>ABZ|1;Y zTJJ}TV2>$8Dwb*dMF?rIUgJY1%B};)Cx(nN0fL_hJ^zx{Nt)y&1p6#p3S0EOCpuT$ z9|b!49~QuP_4|*6_MLFrBC!YRYK}~mn!olA|tZ+O*PHOycT@*Wo>z| z!Q+YMN8v!{?S{*oFb)JjCu)W$v~}n9qr1@zm^c2=HaSol7<|xnb?xmb!3OPp6pqAK z5oY72D1$z|8z(TLoIbz2^nC0z4sqF$ukKENCHn%I5yJmH?Xs9?U4zJ7q?QDrS5{IB z$CGLzB5Q*#A<>sxCoeC4yDN+vbs_x3q#ROng+q?+b3p{~L)^61&@tFkc~|K661*AV z>~IxPj=K!&PgN9NWex=XdKY;#TYJ@|#uchd-p82Oz^uH_79Z0I&>)vbA!?}MEStns zbZm5g)Y48Pp*%OjYI<+1YYb2iNFxE0a>PVsb2*hzp5NCD5pvR}L>XJqJ#R1$no@rV z!4$Onbmz!&;A85L!Lkr4rz82xcaAFblb<7ztt}gVT_lKklN>?I{aaZU)oG zMQNme+Lmv~=3haEG9^;G8zNj7$bbagO#ZGCQ#5zox54n}9(?z5EUuOO{WyNeNkGen z)L>7ja)TGUcV$Rmy7nZ8?O>gDtCYhnA(@YmGfIm7`^$xvQHYueVzXh_fV2O8zbZ>< zyt8}O+ywa0tk79LZKC945_FgpYTDvi&8kXc75T}C7jsEnOf1nN9qTx4(WrjPNJDdx|fN|bzEOdXYP<81VkYNLJDyTDGyu1u9Obl zb(62FpwK@EPvEG}J$-yOBJZyk+U|=*odF(^T9m?T{n6^pp3i%-Z!vUU(2+6_uv9j% zK?SVXT_FI#Vadi@M?O+b`N3pdO!!V47Jx0+gqbYd>j>BjpVqCV{zhV09T8SNR>T68 z<#9QQBne5!6^LE1ZVuafS zo2Y?D+tL{-gi!%l;Hgizv{-HmFqzjXXIlCXad)9m%e`De=DN2qz%tV}50$ZquP)iT zB{7s!3&bz2+PJ*5b3U2fYfJdiZxnIU=}WE2S3kWBKP3mGyeCv4iz=#eMiEO-31S*9 zlFVN_Ed|=BVj`PCBBLiib#z+`Y)cO2_cb*f;4nSXAezYq4_{Km1O1B+iQJzQ&gyjB z-U5PSNAQ0$Bg@8;g)RS*l33ad+4K7E>zjQyua5 z@9zmY`N3B~a%$`HAQwV-QRd@=cHhPbx9++dcM%;Wsmw(JWs6^vp+_SZi!@KDJ8G+) z<6XjnI!P?UIA7<*gl(q{o(EAojy4r97PV=b*NKN2EBW270gaL_7P@(8@KmIU9!EM0 zOSE=po5zT3Vfc3)m&7|)^84Y|oPP2CSwr`DyVcUbx_2BBLnKMjH$&wP!=8@LvQ-vr zMvH8|m!*v!2l{NwBvW55;$J9`Zma7ls=yABSF+)&B`8kE5f$xX1`EMlZ(tl)BUmnY z!TNeO(oi2n5Z=PPNa*63epFpgphiVe72*4F6V6w>$PDH)eUo{ym6S_$;xM?HsRx(tl{utOFAzjN~`N3y{>(>9*aVO`>*+7 zX(t7Be=h!Mks|oP+%{Hv6y}CV0AqViu_=WRuFu{dc869hGnqMZ{$c|45yF3qN~uk{ z{p2A-nZ1TpiaeCw(Q%#TN0n=lCW!bpEa+5zM7|+x6h)r}+c0sDH5oWW)hfp`J*sHb z72qEAZawt4RePA1R^~gh*3#|IM?=ZX8s{9sx@H9rv-ZZf#2s6%o9p^! zy)~h$6A%=t-<>jjZ6WI)9CEGdS)h*u<_h$)CuV{PX7xdjDJ-E>80KB9{88{4g{ z$7@jD+E0cOg{upqrcLamH?d|<)S|NQr6^^0P{E$YI_aBnHgI?GntN_3KB)f z9&c9(+SA-`DZOL+Ir}L+m=vFl{N_fBD@3J)ILqnlNU0;DVlo3_Zxm>FIX`Z2QBWV{ zXcQocDhXZ~y?16QTA%!@x<<^}mu8==&a~Sj zYKwVNZ3md!`^-3N+T=z#vD|ncH-mNO@wHCnjB&bbINJ^*xb=Q%-(st{f%+?<3_`hS z?;jE;+vIDi zS#iNb2%_}1*t#pG^pYSF#z8t1<9n}xy+1)wCNsP(%gDn&m*MVR9_TaeP0dmHZY#Ja==gutl9QnOr47(_rJZLci{y6D8WVbOD}9QS?WQx zllnJ8mbf#0mymcHCCQtbMP_T%0~R!pD3A(16fM69rk)QfEx?Suq3j}QmqZn<+Qz@6 zr#o|^l{yI`hT=`oJ2Xu2+6(EO^@i^aWq(=tw!pNdPbGVob&R&Qm1DT4?Ls+9*Pg{C z%8SN*4h!V-EV^bYAHj|>djkD5boG;AJBZSctx8%WA^C4U2P!>Yq|9sUo8a$^f6`58#=?Ak z$USn+c)ZnWS}C{~xj}bk?LG!n6oUG00>$uQ4fhts{$+x{*S0E~xUo*&T6455h@$aP z%qCf+7cdG^t@Qn?F83n7nXxRlCN>9?j-N|Jj>--r69gPycC0sx^0_SiJ_>k{`uY&) z(g#qVthNSd2U_emx_P7E#o>XvH=m^FU9e5moKq(M^?0K~d(oEt`-|EhyLw@-MB9iX z>Gc#mu;9%Hbn<;JyJW}eoyjPt@p{D4UO!Pwze>6TY0M+orGfUDO}NOJI`!;lkRM<& zLM~t;t@YQIh;fHV*4Duj8>{5Z9`477?oWPCt}UEG1D;zv-mO?N3s{l}@sBYlp|V01 zO(+X1s;$Xhx?e?5W17MQ+b0OnZaY|ggiy1?uAtKZne)S%Z1*j?@a>)S1k*0P>7>>8 zp5=pUjqx#Y-6)6Dw(4@}Z~bF9@ZPuq+R$GuxuL8oB-?|bhDAQ)-|(S{#J5$W?}6IC z25GZ5o?EqMLS<;g60%=(da5{;s_)rQX8gKx&l|_5 zT$kSAOJHNPlKbNEGoJ!p?ULX|Y$gxdSEq;?Q6}a0mu?R{(Qf;vg}NED{_bwlc5+O` z^LuLlmQ+ z{)@f%k6Dd?cUI~uw)EH$2w-jTdKQLqi3x^R>q&cCeA@X{eTgP5xpKV`b^!(3@43+5 zgd=wWxT6rT~MRta(!MrzwDQ~^OF~n#_{>NRDVLc#pR3r+83Tlsa?V@ zc$_W4^$zKD){Itq&X=`S&C@08&B=Z55T%dL<^Ak@m1!@~J7W}Wf7_!>zf0>L+d5tG zd5&bqb=a+9_w5;fDD6Px{1Nh%){j2GeW%{6Vma8CYdeJK%1$&?=5?_xK%=z~1scwA zM?U2vRHn}8uDp}#l7&YGi3>s$FkH~(Zu1e_4hw55Yo-&QXHl3wxJ*OKjL-6`-1D>K8o(-Es z7thz2T@tNVW3msdu$X$p5Mv1QTIMCOS*st-CZ{W)vp-!UQ&^x=bH0$FV1sDtBG&=d z%pr91jNZoL({p%!Dub+Wzx#9h?44IhoMuowj{q2s3s=KnPV2r}#TE^ftQum53&U-lG zxtd5s{^mjV`GE`eZiEUTqbIpB)Pob1EtkmjeL|gRwS@?@jMndLDI9$I>!-?o#hUNJ zq~-1zpD#nE(R{Q1=lu3O{r{S4UOv&RerBKVwnC@g`Vuz9%8R=ST4IsL2y2>RUAQLj z{0HHh;Lk&;h_j95$Sxd5n!sKgQA9Nu*f|5+mjPS!bno`Ui6T92PaX%Rz#TaW=O^8S91C^BJ1R5{mTv!y?L>&$CAc5TxAfI;4x zV@lbW8ckbLFR-p7RnDKX?ImsywYF+}UYT<3(OVW5kZdf%IhEvgc=RiI!!EbSOsBJs zt|mn_wmdri5tq3FSMD?^P{@xtQrOB$({l3efxf}&*5B3TP&_B~@sWT}?kSfpyUa_n zeIo7fpv8)e&7SA6rWZCYsuu#iB%}Ezs-7InOJT(b<>@{0`XC@HKT0#SH2s)#3iW1( z6P^%jseZOh-^Z+U0aX7G>G!Uc=aYYVrpaLF zg?ruu2bl8)>-xp#X|UTa_))?F=4|)tQ##;suR{nwLR(6_f|xlKPyMMIQ6{&3*6lJ; z(=09#Nab!b{V1qdH?l8hH>vtJYmltuqtyuS=@)j?$DNyf8IL?shhK^w!HifHDOKpX zRx1++yR_9heTd%VW}XA(fP6TUq0emFIME%35_L{rZS2|x9RwAzONC7Lskf~4Z^~Td z#cUT;A4v|7u$KF~;orLlyS!w#ZL;Zz29{s5qfzTU zqp}>S>Q9p7EcvLqGqeiDC)Qya0&4-oq*iQTpmj`dAJae(=FkB;ti^$`*DUb5TR?M>cQ0mAv}v~AyE*WB-d#2^GPeo^AWE0GlPs*q$?H-7UjtS{dV-Z327Gw zkhLLoy(<>rQ-6+~$&qyEN{)S~4tPk#EF^tu6(b+6KFn2!$;!7*(yXO~l~bwBBs`uR z=`KF3-VjrB8Y!A?^k6~0DR`aJcJ3KPwjgzuUv`T|kL+cNAIT_9G9~Zi#csWaDJ24* zB)*ACiUoCU?kh21h_Whqhy>wBDEs)Li>vrqs0t27%(VL`vPRPMtwb*>Drc+aT!H=; zP{huf3hXQuY4sCgYEL@FHQXwffgXGb74oQ|r@~hy$Uvuda zS2F@qHX4RwTI-TNY)mEnhy@EWrDOStM)9=*Lv0}rrs(PUjHE+zQ(j#IeWx8E)E-dV zcnyrr>QK%XXzEw|1&`Y+*|%F_i+sFJ=I(!(3p^Q;R4e+dB*x8*QB%;FAytM9D;1hm zrr$+fwksC8AHNvWtr}tj#PDuqADTAD?$SMWd~PheSSg;Q`K46=noKJG0DARgyztAn zAbaug+J>I6-mc-~B?|_E_z_CW3jX7$DD}}EC!+~A0Gac{HEgJ=t`0w05`5lZ!qYf9 zRnz7U3qJ0$|q4!-A;>OUh!j=;*9K<-ZgjmlRT4Xyl-qhYdUvO zwzM#*zvZu2j+s28Vzf+>D#dsJ$*!1@dlvl)5o3WSec7^y4!6Tb?*7l3J3HAa+)=zt zy~U4Px&{Z94kS?m=+KI6qx@jc!^p1DKF4n-h8E#(cZwm2_o&qapaM#3#x=nqRx}@W z?ANn%J-uu%Ze2s1qh_+_~( zG2lB^Bwl2yI{R7XGx)%HMsLO40I+u6abDbmgK^vSj#X^#zCY!j@*%XkVD{U1w(liC z2!2F=%)+m>J$OBBYLyV>a=`B6V(EOP!;qJBhVn5`cF=BLYyS+PueV!ph-XH&S{N_R zeUVhVZu;MgX;q>@m=7tF;Zxscmm(bXBKEUHqAH z_FlUv%Q#!-3%6LDnY!TLkUfIX6_iYsZ8VOYK=d{|R*REgi>uA4G;ksSx_-(_y6n`6 z817?5iqa_OUMFjvyNg{Kv_7R9dMWC3{_eE&298f}P$o9i{2VxQ!zx$aYCZ-xPtji2 zOfMqGp?4HUOBeFJ9H$DhvEycr_S+K%)(GATLD{q10Z` zkK!RXAC{Z`V^UerJbRhU6D*`0Nb(8RX>%5-Juc{)IT}|I_>}?sNr*+AzQh6n_kayZ z;J!KR1vaR8X~_or_Ccf~G!zQD;g32c#qvY2M1vBF?||?q;0+ppQB@E_^#4MJD3G5F z_P_8)(p42`Ua(?}!cP<%1Saq!&;{*M@>vi{0_HD6aDM<+V@|0PEAM;-x>W%Rx(5Y) zmBIc&)Jy@|Wt)8l0?bm+^E|P^dWr=30f_;g;xeF+5zDO?Fi^28wPOAM5%Dw{RJ!1b z4KnDOy6Z=!3;OWL5e@1d$M$PUsJO!bq?g3r#W{-u`Dq*}U? zKg5Oy*}bVe)V)2ll!6|NHCg%We$Zw^{Sg-gSW^c=pa9^{zYxgwUwJ_Iuay6V|6cw7 zk>E564^^js|48DrXnUVIwum?cQzME z@$vUecYCjEdzU{fk@{6ICT|pEv%LWNQU*ooFq}D(qj(KE27N2`vQEw@aNrHpdXv1P zH|@#{q`+CR76VoFMYp-#b2ey0jiRrOUSOraeZ)Q+)RzZ&xH=?soqWBxeGh*ACpJv^ zvm{^v|1wh2Ve$^@4d>Z3k1ILYK({(#NH{(4$k*pJB(Z9WQixv|b2E#W3%tS-_4&|x z4+?Zg3p@0H3)#B;hK_&wf#Wm!^vDNWwqf@ON;qlE^djuk$#aW3uJvfwnMcNKhAL}A z2LpsPVfzioHY*p)xup`#-5$+jmj*0C2JMqA-JAV%C06>LXMxP$Y}ui&n@zWem5*!~ zFoL1BA}Nb=HI|2^sdwuoDAICz4qt%iL`~T5pq$d*=QOzW3$&W_>gMFALq0m)wN}qP zIM-gBvb70TufMdT?=>a;PG81SE#j^aZzt3|%ND&PzVilFx6C&h-?Ypf+8;f5vx~nn}OqmvPYQ(mb|5&50o#@%F zEkgPqoxRaf;i^`zCx+_VHs?M6yBy^1`jvZ5e_85{zDw63G~~7@tdC1PL0O#=CwT=L zV@kC}t?5aS;XNVSb>2MW?xeE&$6-qe(0kj#kiqB9+Y@cMtz|dPYef%~9M3ZBl8zhi zVY0^rsucTF#!ic8?5SZ~TVg~rLPT+0`f~#Ezl$p!F6A0U%LT1Byi%kGA8b5CA$9gs zbR4GfP~*1tskJUP%?W(7`7W@wpDqtxcvG*}FmDB%cAR&`FCu(VyG0#XyTSJ`0zp|; z`!U66Q8hi^P}jI@m*(j%4Vas=MXZLGvGiOalaKOR!wb&W#(L>ODtq4=I!D(o4Fa+d z^t^rP2AZFeH^!A=lqII*mEtGq(uv>1mWZ6HH9^NQ%x_>WNk;-mGW~>onigiH(=xiB zqhm*R(m^=A&SFB%Nd6w9P^-^016s(oO$ZU z4X`ZsOy}pBoK{WgHXf9t08boIQX}2i7_mq@%aS2eYfK0)$R#Lmytc6mndPX(TFqp)XYnUz>pq0x4Si&g8A4bI)Uo8->*ulmK4c8t8+7m*zG6Gzq%Ik8( za(>x%Jpra(7anSe0+u5#mNJv(x$G%dult}z^h^Wtk=eb{0u!|7r7yG@=$@BxnJhU4`d(h{c|Kgwic;fiYu} ze6jDIrOiP&FJv|#R8nUfBEVy6R?D&bFeoS zi6#DMqAe%nQ+gM54%qWu33+2zZT#z&Y%kSp?xB&OSBU?5-kzx0;@B4wr0?O2hU1aCd4^HD~*`Y^A zNl!W?%V&gFHtt~_!;W3!%aZCV{XbyBTO2EE>F|Cqu|W@dVp?tDI*xchIJ%i$xM@Ou zep~V;b~C6m%04Y~-V98>2X}QFP>DeRBo+Zoj^b5h3VJ{aQhnm}e)+n8w6A=A$e114 z)zz6$dM)Uf+cs}yJZd-{`uAJ9P%m-&9V8z15h)AOcBQk|V?+|TR6)SK2iUJm& zQ>L>QCtv%b+@vd-{4xwg-Yo~&PHVu3Q=7;#m79ZXuPRMBVegS4Kh)+=U_5teaix&+#J^~rhsh# zv3IV|>3hY$7qeS{(`Rn(u;e5US8*kEk3?8%Z$gt8LOD*gNqi-SQw-hgC2-=s%|Yxw zFn4LIc3s=$Tv$8vq4mltpE$!cRDy@5I zjc4)H)p%$1+`Lxx=b@&v1zw-?5rZ0GQm%+=nV-7AqQ{OyuuI@;g)M^g{hN62Bakh? zn%<7B|3csoz;OPb5a2xj@t;5lyaC+)FZ@3VfMow)6YQqjI{?gdiOb;PEeH93*vEH! zGfLQ?rb$;ZqFWXd7wmT~Ij4aqv+6p5YrlCLLx9tp0w-peK~+4|F1#9bZ06!)C(irubxQig z5h9_jW8u%Aj2GLeBgz@Uo8%P@;rhnDu5H%xvkTp(s639@ht3DGrq*GA@^C{82l2&t z)C}a*C*Cfy!?@`&wB&8IhCs(U=-`fPd! zzgkL6HR_0O#bUOy^qLHA0_BeO^OARls&2ukvkVq}2f9u|uzdnQ|vp(uBt@Qzx!xkpEZG=t)>PURQlcF&eyoXMR0$=C^9d1fD{dA3Gy zV$MAw*B3_s2RCudnG0U%yM->EW*@C(*PT9XXPgKMW+kqF9-*?Mb}NZW4{fn5`Y^43 z)Dqn=c|y%T`pKle$6$X`+wmddBx*XHX#c|a()xRh&zfd%00~VDOZu$4qYm;&HB0dI z_!ihBGp#6X9S^GY1S%^SV3Eccx^GXbEll(wvyd&wP{|E(wC0>#@*WC#t0o~13<#FY zex^S>-ZvM|sX|VMhTjsSw!+MyF-p+a)0NUO9nWhOchncf?PM%B^v2HLu(RQZ?nxMO66IB_2Y=g-AXoVSXsv||k8mnLn z7+!Dn#~luD>B*1dH@Tj7FhW;cSGtH`l;1*oNT~O8kgbc1MXE;9>D`~Rru@^1>?{RB z2*_flRRB0*gZ9Jfsfj73?IE|Nle5QJ>PVR$rnFf#=xaWzch7@%>)obh)b6kGSVcZ< zq=0TwYE;BOHD^MZScg^-z~5%^Z$%m^-tfUPdv-$>I3)JlboiQvqpq*6wpbkK&j8mY zK3N~?=3Err$SJJ0xC$ljz1(*@-(BOpfUO(K!#r!!H-i$T1?c+IRbcsX40*lm4Bk%# z7F#;7Fz0kAo~?w2qxW9<+DNDR>55{<^|KuT&+mfhD&L;RwT)M+L$!@3csQWT7hBiS z{Y-|nqkk3}jKR=>t&)!ATJ*K|v8;gfM;a-`VQ=O)ivu@aQfj>}ZrwZ6De2Q}NiM%k zB+HwEmTc{>3zJ6}2tG$wPANO+i3+a?j2=`?`~G^omekru2HLHF%If>ukWOT*Mb9kb zDSgV)Uj=ljk6Pf(0BTXyGnG?VQ~_-rCKOaBIIBOg-s+K_YZ=W<7kh(Qfc6g*ev zuPR;+;`I+``!t9A9a@2P#o~fa;do}++GDv_n@YIWqrLAu1*bT`GbSY*sP}IZ<5ICr z#<>5mwO3pOc4el=9`W5Xd#To^Lf|#}c8J?RV#{t6u~5oIJfweV{t|rTxv$7_c15?{ z@%aXkz9|9F*!f&ceHUb#NGTdBsP)<8;(76^hsUx2s@y7g{dohdPWoca_T+4NV56nS z5?OhIa4+hqq%PLWYpky`a!ozfFQH``B2uihEbZv)*gMA97pfR0ls-ZC zqz{xlmSWcVFn~_sDP;1F?$(b?vBuFE!cSRnlrJ|0#xV>%Xgi`#$v886Ji+sgHf$-? zxUpvNH~S|O0fyT0An0MI(w7y(kK=A$q4Uzl? zdJu+2q$2&H7S|K5F{o1`q|DVAO&^!3+t@oO2*lDTfWq} z;qU!yk`O4`XeQ$@DO0{-PObg+SKO<RP$PbMzgvu&wChVCOvCu*s5Dc$KSKGna&8p1*gCuz+{M_piUKMxr4R1~Xz`NTS$ zT})1B?8l|F{VwtCRjEC?X=wb*7={LL8qmCrbJv5LIV)-yKmwnlw8CM^s|!Uzq%9e= zel)oQW%wvBG`We3xra~NVdb1-d7^-_Ylr<~F2FCabu!==_^CCIFkaYt9%%4|5#^V> z#rVscIoCBUmu4zH$3AFMm3a44C*whxzd<9Oc8e~&|PAS;ExY7W(ER>6gdTnmr?5*EBN{<%8vyT{a6<%J;? ze~Vu(i2l5#BoIdq>k|Y1IqI?hBdtz`v#N6j(5E17gWZ^M~zhO^i^x>N-@ zvJWwwBfp!kUXuSYQpdk72h2tdaVvU&TA^i_XlnA8$<)q3=!MDLdDp+Jo`e0HwTFgn z#h(H8N%OZtp25<6a4Msft?=7P)hSlR8ZkO#?1jCw)C99s$eFwu&0UN>>m6(aYl|jf zm`OAnh``As8uxOcNYnfVoalb`j4)azXu3%*95tka15;Xgc%9G6S3M^f z1fu*D((Ld=AT=#^Q^YWX3Gix9Uh(H~?RaV8H_g*Pj!QMq=hff8ljUg8`*W!p-F;so zOn&Hi$4t)k+iL8CvMX$NzbV`=l^|63ajsECUS{r7?-^(3n2q7FkxJJaG5PQjuNIZf z8!w9d;`4dGd~p^%UI@?&!mn2scxG~qKSQyg5YHNNosDHHbS1Mjntjx&va@9B@Jw%|rR2;0vh7EtCC9NtnrZV#l?(AU7Rw@Uhpm#F0uS2yHT`T0!m zqe95Zx7_D$mGLvSuGbfD_I$jn z%6I#|9Xh>Z=I-<-8Lws>S0(ryBKY;d8`=T73WZ@`q zm=z*37yRAc;R2^#i6&9G%fsKb#n2ZAc3-(WVnjuqg-MvL#B|wbYMvy;e<~N9tQ^ox zChA}=iDCp1LTv=FMtygil(^{*(2xVWES#(3vUx1oQvNEzLaAXJIvQ9*R|@5XkIsFl z_dB~GE4IEk{{<|hQ*AoV?eJ-5BerbQJhP(ttmAuQLIb83x9-M|V63&6vr_bz35%lU zwiW}SepcHsGO%_f1w|;Ph5mi2h)p_cxCW6i_w^u#$vi};kG6PmZ|?>Rb+LXBNM8Z#5#IkwW3%}NwBj3zJDbi>-H&S!`Tb^>i z{!2F^5EOaHD&tm?aABtox0Qc~0>OT9mkS7)ekn?V-q5AQXfvDDPj6}1e$@H}6K{D$ zM+G=L@9O&M5`YU`WQg+ff5xK2ls`lTrbT|Q_<(Hrp0qa{^E3|OFABML&cKQVeB`Vh zVkuZj;}>=`>Vg$nsPyApkuh}1nHQP0a_l?&=9xcJh;9&}jv4XGD3q99qR3+>O34Mz zUdsbsXvoTzuX$8fxg8b^oBW=jLfrAmlEQs{u3DKhy*mLxPCgR#;fjgO5zQrBbnZUg zN8GvP))B+sY-2S@K_T)!cf4mv7$cU(8cb>L4lAA=8*6DWzqE;Rs~Bp4N$YCX~g1 zS9s<0715$BGP|9F$6z*5yxrE2-wpMJ-JSD2sU41}>g=!CzWfkqi$GhOJJ-2}f?j+-4k&*uLR}x3! zDmVE>S%UTBoObGA?>3GiX7H7M!oybnAM)PvA*${R8y*2ARAQtg2RMMVAQI9&Ln;E& zNH<7JgX93h%#e~&A|N0wtw;^2bO=aGch`HkfA8}*JRhF(3C@|bW39c{-e<3CU8}BA zZeEt^j$ITv6Dqyl#od`Rb#gneU^;FK^#&hsxbuzxE`Hx4G3^r(4#3VQVV10%YDpw} zDlL%441t1Xtjk7=f3rgMJb($+7B%X^gwN}a4<5k%&7j9r`en<=RN(B_S2am*DjZx1@TALma_XsYwNtSjDFmX02&>zMfsyldN1y*+QvK7BL~f?N^vU~ zAHqjlWyfN0^RZJuDy3!p^FkQPAHtQT8hi@Wk$kklI1&Gud!-PRsJ%vA%8`Qf-b;z^ zo7b=+`gd0Xs6Rz=IpZ=@EJPHs%_dC2Z7K9qJVSiE-E4JE51Pt4gnv87zN+-P>Y6^x zslnVcF>Ufp7c_ZadXF4}Z5Gu|5D=PRXAzuilk5nzf&FH^e^)WM?bCV}E+P9JB6bAnUF9A%yNy(wN9cUf_3XU(CXUgIh3b z$!+LuChLc!%(I4mf%#rd)z>bbO^Y=Jm0R263Veqco-#+aUV37LYXCiE%Oga0!=IsJ z3G)rXU-d{{^spQ*vQVB68iKiv34fhtd3PSQA(8plRR4)%wiXv7TxZD*px8$2N+&Y4{6toWcQ}qSqmecort@9hOzMQ)TPM$uJ~RJikE@O1b@K5`^N%Tpg5tz8~54HEmkfzL=G z4-H>5Mp{>Mzj818O1%)fFb_m^W-^wE3BPVM20fd1kYA>9(C=^%mHd=Sby9%Y!pg2H zE+W}f5tPP1F&wNC9n^c>+qlJQ(BT&RW5X?s)~SqC_@b=z+`p#{(3saNm1=QIy{OEi zZ#1yoHw$(%bgPQ`?9KOkY16AtxRclokFn4I)4GwV{*F)7{R*@HGdwwF=DJNOs^y}%j6dDD+3q7 z8QJCq?-vZ8w)f4a-pykfRZ}lD4}Ee6o)YntQhu>NdgM1(4?%>*59F5uWa+19n9}Ry zJaSxt;%e7d?u@cbBqTtw=BUGO7SFd-@a0Ez`qk~Y7|{f48b^jU6e{wB9QAnE_NMmb zhE^qdXg(E$@PJwx6UT%M1_UJ>R%&xqrhQ|47(L7b=aSXV*9ER76AY8dxvJm51u%Lw zDXHXMWO0AYqFo)We@eJ$Mmg`a8Cy+gdIuns7w@6mi6xHk9{IuaCc@m>CG%oq1yvzP zq6(!bo*TmeCwRdZ%T&GwP74VLs_KLC<|cknUiFf4=?tcX6z(HOTe2<}+_s8gV-*1q z5@6_Z#N+IwdGwjpSjPc6?vVCVK4-G(^)@3NT<{*<3@)@z@4f;D%cR&7co9twZC(ZU zdHuTGX+Q40#CP_B{=*d_r`LXOIvzArf-Bj9fBWq+t;t!FyFnJ06{EB!D9gL`&M?~t zTJuzjsu0BbEyTK}6aB2u;NvCKSNi;vg?qr*6YUl?_N#Wpepci#J_VjBXZ77cMV?y< zz}wJ_t{B~at4DMXrHTW-lZk1!|G0%Lf(bO^0tFLdoY0U(9Cu|N8iAZUV!y4Avc9w2 z80<2c$Ssz{$q^pb80HtWzFKw>TljEjagA9-UcU6;Cpt_WD$@RB@lX=;(bE^NhJ_!P zO5#+$8fj%hh%5e{8?Di7@J%XPt}p6>`w>1vK6>-cpBCn_3!{PSYRLdw%p8kV5co!N z(Oy#UG#;jc(kGD$>^a`VRP5iOk}x&tR?1sQkS`EMSPKsJa|pBrI_KPmqFcGwGG(Ar z)9A{UOpmQt5{bx|5MocSvZ>%6>Z5^W6C)R6`%vFf4g-2yh27wxKW>3=rgP~Eb`N4V zX-$gsq1?z3X4r4hcQ44uz~z~FEff_U#SXKjyoDXWLp6p@1-v%QE+YZvT0hSsnV25d zj+cD&H1d@%>sm9)_td{vo^Kl%!bnPt48*Z-`r9c20Q3o|>rtFy>|Ru&v~sPH4)?HU zGGtq?gF~(Qx*3140fc>My=4l!FxbOM&5CUlCI^n}M~%M#)Z4 zOuQUiYj{yGIt8-h9~N3SPf9Y1qGmF!6a{#hUi?OxjA1m5Zaa6dr9z^OEL)fL-=zsL zIgRXBLoqI|!CVZa_EWv`xEng(E5eyP#H+4{c@64_FX2qtLu9KVC<~pxdHbPJdlT+Q z(H=4p-pBh%?{$)S@?&g{K>~=^XAX)HGQc4l6MB!omC6@d~%>)J^!V$?oS^@Z5Yx+JTLR>Av-64*rDzv1F51O$v4Gn%qU*x{2HW z-ies1e8fB#tG6jQuphWdKq%U^xUtY9V9xb+!cZ2|HvICCVSq^afmUaT6aYBriT9V5 zm6dVI4z@frrz`i$jMU*G8qcJ># zJD(%?e0|#Wct(e99RkJ<4Rrc;(#neV2F^?q)topy9jPn#W+3T0{<9_~;_4+#{^CP^UPF_W!g1A_;Yz{9 zt~oE?qlvfpyKW#(_4VJu%m&z_9Lp@8T9AT)2`Iw-Woyu(!|frD>dRsD7W@e0k$XZF zmZRri93nSGxLh)cgafx_TVoAOGjI2) z3`cyU%Au33aAqfT8e$x7Jo$YA_hdCF+UOD{P;VrU?U%MX^n_1t- zyg!&4@wsHzK^?GbaD-a4&~3HJrYOA}_p@LA^HVRQbh$D&LeJ*^U;$d1i zQP_ADDqmndU2_}Gy$Q>U%Hj78N?85n6-_J)eZ+*6qJuFBs=YK4*Vq#rnazNY6TtxA z<(;=YVpE^E9axjs)fAMB%X*vYRxG|5f%1Wz>_(CjlDx~=s_)WfS)+h%vMhB#5TtsE ztG48NyVP9hk1sx<3SUR?dyerix6+WpWuEXzCoz(@U3t~2E9&6 zExLiN#KOOdps|I#W+YFpadhJ+&U0H-{GC~cdQMM-P>;=ll1zmHMOqmM?t;R*6zE&f z7pFhiJ=kkvwL&_3V}PMxGVnwt4))vRUZ}+ws+z57#(}qnzoJ~=N2p^BC1p-A&yD(y z-oYp94=^(OL5r2Y^%i$7=u|@&1?69S+C>koiDDGG!I0S~kx!P-w3^vPS00W~0GwVu zuT+_#O@>9X@`aII6Hqse#TRPJV5*)Vzt25o4Z|UhAHMZ(k(i{f5$#pq)bW+&?Np7? zZ>hZ7-eWErW?ByJi=?Xe+cPo$teJrRf;I<%>`+n)*o&uId9j*f>dYzg!*^xBap%QV zUp;?HDk&X`hgUyFY0&k(!YQ=w9lL`8zgWYo)vb(&BsU2GKrkw4MorrTA2Ot|fx7~{ zV?fw?jEB);qp`R#q@lKGlaQ5-6VuJq+GH%kBdh8)@P}PnEx^rR2z`*Ka<=4gE$gy! ze2-8{0J@9TjCX9Pj}3DFPc$1b^MjfB=r#{$z-XBQ!^FoWDO!N}@|r}h%Bu42x=n}c zp?ig5?fCr9bomwZe${UJ6SSy-VKHf(mEgDi^JUT*+LnHuHK{FPAM2jn3N=xKUm$!K zWKkgTdBG2bx|sqV&Ahw5)$r8H4{=H`+hwj zAk-xdpGy*I-pF>+bC9D8^Zo*ezoursDa{k%O}PM%x|)-Gpg730-l+uAt39h|SK464 z=&AWyA|mOP$c)Or{fkX3DrEg5se0g1&pevb}z)| zLF%h@la`m%_p)%jQCJ@j-F;*J%#j&_UXk-H&i$$@VBo9iT8ky&}Fummvb911VJi*;Nl+{=C|C%iW(3;kvHW&LYCv9O$3 z@*|IdH4=DeWEi)^9k>7UY5;lSV@z~c&RSxqS4jL9A}nqgwW9d9nN9)(X+eips@eFn z((plyd}-NyXPA5pGm`Q=@xPQ}V7<-s0~2)=PoQqQfQH!uFcO7IXTaderU}yTe^*xS z+j7Y~wC`@66#C?Tp}XAIsr<`!K(op4?#*ZgFOwK<;9){2xn)OjHJInK{O5y_Y_HG% zU=fMIcQ2JI{v4ZjnXb$J@3-Gy#bYOrjxhI`@H9_C7s;>N`C=}XF8}usVggwbfV(=a zJ77kuNHH&w(V$=Z<~ng_ElfaHt*9g}hZ(>>vi$FEkW?Npq3{rSvGT~S1*8)A-xU~( zF>znhnd%E&bQhcdFJ=dPaP??Fu-M{b8zp9_B=r6N|I;gI72?S$TVtYS%=-WO0&eoB zH`$7AL#P#sGEmep3b!9TZTfqewM!7iET!!GW$1S`2EoHA=*V~0H5E>p>9c2^3|f)uoW@wNE}$$)k^ zVp$B_?q0-v%VaTq~akHhzV@#{@7q>VkDLylZzYLDEvfhYO3xTC>Nm;$`Nyo9M zo7`Xv>+4+A)oNH<@tGpyLcD$Zl185k+IXPmgd>(CZPuqX{q#%E8_f!U`+-0#QzA;3)d?ZB|d zA;1~|=9^DnBJ7k~V)IbXv*sWjrlnNZ7PtRM& z85k+scf9=?H>jCD)P&^G!0MoYrG6-aRH5@l+~FLB0?L7yYKl&u^*vv|*hC3Nq`ziq z0SO{|g0h6S*3kws0`88!FNh>G!m4d`ethIGD7b~Wf^irNd|-%Ew9EhsV4@{(Z0mwe zpQsModQtg&&MQwVP&MB*&WEO0ghMP|qyIdG_df)sy}rl&;_{@P9*CGrUfbB6+g(+& zL_1t~g8GPyDm~~@KrQNl#Pz2EsH!nk+lu!a{`mfK2B=g~6IQB007yk^PnFm}SCj|}s-^ZzW?=v^%@|A?7U0bzl95QDr~OYig`LcDRi;-N59IIZ zG&SsW=mAi<_Q7wb{zzzZMG`M<7Ee1`QCO>Pj_V%+LF9(Cl2(ozth5tnx@cuz))3s}ml+GoA;MUt-=dRZxP z!6l26**4Fw0(4*PIVk?za;<)QQeKK14tCu-nI!$|2r*`2<2kVVOB*;Q#4JM)-ml%R z4yqpePsT*&@xh~>(DAj{-$&<_oElwB`d^QvBs?2c!pl5lg@uQRMZeT@rM5Y-A?{1}E5 z>qJ|R?(&m?wF+#-7g3(#nd49YCO7NE&>TfQ$TDlO4AcG3BW&Be{zBUO3*Pz_J^8d* zN#Z>n1~&))3dI4!xzo&u;WQ&JHNzIF%8|~dv7RzqL@Vs~74`w=nenld`*-Gp0sk@g zrg`FpT)|zbJk~O;2gqlsTf3Qaj(f#bnL$D80WFMII=@To*oyA*8&q;Sj14(5mby;8 zXvlf~Y*%%*@HykDlPG-N0rj&viEEcwxWazTv3Ga-vcP^z@7Io*NxGBCi~KHQ_9W+7@ZpSFE{ z`_xKeuJ0~*9{X^5hGe`VIYF+KnJo8FY2ZWn7k*8E{%*%xE511YuWY7azJqSbjSt4H zyy>gon739YzWM&FBD#?8edSG7v@s< zu-~COwKRK~LK^09IZX%UgkY<_HQ(ZkpY)~;CqFcY&Fu}9T0}=!*M#3TKlS9l-K@4M z{bATR^Rjqr$uQ3iz9_Cz^AERqp_t^;<*HkP)1v`}S5kpZIR#;9eI4OW_Km?epoqdg zI#kRZ+EEdJ$Z`6rQ+lsocHi6a0=^Ss^v-YBCa~hcfn3m{2QeZsFXUR!t<8>C^PaRo ziMo1Hd52Bw0aGe0lKL(lQw~rQE&nhDIo&=W_b?Yp6XOjf+o*ccbmVbo$ zrN4d|x#-@*?jJOnMJe&KcXRmf{d+g=J7k$fm9u*QK?7?&OVlyZwd^miTNrAHl`EW0 zpugCwqp88MNY3mw?8D2lpH=P?v(d)XmCWk{x#^%k{@Eq&U0LZa_*Bx6t7Fa#}OP4AU5uP;dti{N@$yw$t|ENBkLl1ysG*AgmJZAwX*$A{cFRJq)KqA{!)RJ z-@$e&Mow{~{*jhU8I+JQ{WE}IMR=nYQSdy{u_kRBw@0u!B4Gd}espg05eM3GEPX-s zlK~HYq4QoCq4iEqfG0ka-qWtEPD}DbZa;R@AH(sRo2{^ELNT^R*ag3(lszmhZ8 z1WiO6lc@xUtx#z@@4|P7?)tdUyAtRjGfhuWIv-!93c*u4$(WXd<-=|KBU`dYh-FN0 zFokqbB)P`?x7oNTu`N^TL_AD`LZr-xM{#pZ?_(QifP)#|%46g0)5qMdf-Q#{buE&^ zqnGCR5sN*OpDoZ|sFkk2$?BV1BCQmCE(B=Mev9=PbF6q!1aX~PvcL+uhxx}QCmwVp zT)vO8xU#%5CsRf^R5P*q0J}TE$@Ezolo13|7ju-V;w9gR1NEP{g_4#cgm+k08G=fi z%dQ}a7-vG03cTq*BzaYP%ra>p(*HcE+sz<9r0E{Ykf;622t4zjjOGuf53ii4etBn{ zs6pXx|MW&CFL7*B4O62hwp3-fP@1(gF7&Azq4jj6L_o1D@n)K$M-WYZ=0E-KdP4_! zDoDS75yM^oIXJ5%S8!@FMOvL@yF(o|ogx5KWTXxR_$7>wS<9C*du;B1_(O{vUMfaN z#c8r=<0*_gX5qXPO~;e}=l!e;nCY5Bp@Fj@PG&oSXdG0YmynceZFqC&3I8g}!bQkq zwc#VLd|JP7_|lwUdfLmvDXb@d%L|xkjER*`=@XrA`-4P*Fl881){SU-v?VfrU$v3R zu?$=b9nG5{3%?vu1mT_WOAByXYqyI6UIq`NVG;NkE`Du>-_kO!6I{M|V*L7d?gF?I z-z4k*mGbk<<@KejclZEUwpjXLvzUW8x$&gIWvBWJFCn9ev z@`FI8$X1+l7A*5Egb?(?36cP{?*Vk5nkcXrXpuz=*3Bv{uz(VI)rL#x0yoU`ZC7hI z4#>CaO1#&cmiw($Uo1_I`JsgxZb!Jg0aOIwYr9;VOm}FOs6966bzjyV8A}pfd zpc)2nG36v=i>P~5pGOJcvSxpOjsQujw+KnTcIZrKe%RJICP$@Me@FOwsP}B@@Wmjw zN41C%Y5n=jeV(=+HkIrGHq5^(zj3$;`{z&e8qB(7;2_p3Myu1HqdJS6urKfM{ zrIi(efMJhhxP64&UbxnTNru&CO+G#5tLXVZ;ppy)B+EdL?`d<7HE}1B+L$S@)T?ru zsKQa!>Kqo8TI6p=ozE4wLEEji`go`?!ysa@hHyK8)u1{M2mX-}7kE*jSWSD#^w1`8 zcARNf+RV;sGj427oeo0)ZK%qm(JIJlc5d#yUlmr7_s=)~84Khth+Es*L3GHCP zi)=0(Pl|}ZLw(%fJ8%mEe$(zI%3*LY-GXpBg1Rr8GZeQlb{ZVZ|AH603X3d$DvAI@ z&pfEKe_{m{^SEY^beNrHaj7K>cx_BdiCSiL2|g6<$?1a+u#{9y$Y9w3QL+j}`#aVL zF<-^Cd8xt5aZsdQi&pbBG46>a?;TqMXoVFG3Ek}?0vJ$!tPy!c!Fd9f{@Wu?YWQrj zHSY5W1tPD_X*4I7He7@!y^3Rkwe%L|Z?pfi2*B`8gp7Wl?BHYx+TvL?Er^-*Ry3Os zbXC2D5OFcW11xRobzvOu>RQH+{)0))d!)gjyf&Q3!iwthWhFddR|v!z;06IU61taY zxtvB~hQtNsFo#XJ*n{cJY01wP6D2EIhdb2riEyXFzo_2yok&OD=WukkteP2|wIIR=ZW{=sq4tLe zHRx=i#4UgYe%jV3I}5V75Jp&}xWbD76spsV5`Jf_(%)-(ND_F}EyPTQxD#Qa?z+T9 z)jr6yG76ho7!w}pTvo7!=ew1#+ z;Wp3*_L)_XWrSfhhFHgY^o zN zg%muQlqmhvC;F@(0K0nzKh?2|UUwA~e(419Ru_RY*fl9)~kTh+Unl7O$ zlsH9zeNEw$41%~XP-4R3T9gJD&^7iCh6|gEnaLnx3JotASeDAPCc_-EYFwNP z47vCqt!I6Vqdj3rp^`_(xsmn37CVlJ8j0lFe^)8Wazkobg_GQ12+9%RoGm&7%iqA& zv+6L5cq=_;3&cEg&l+KP&v}nv)1T=1lPA7Q3*Q^AtAg`1u8yv*jEP$~)rI>nms)0K zXP+VF4!)Q5TsAroonB=Mi!Ix(|9jJKQewfM#$%Nsy{DPbd3|wZZ__>d9KM_RzT0!Q z72_{elQHlMQ9kYTQ_2jx?4HX&pOE2Xbh#*n7?Wz<(3thqsA4n20P8J1OiqDi&u-_s zQyIrN?>CQ`m9I6wU0quXQ)%t-{np5{v|L=fqC|mdNDDrT8$rzzAjCV=TA?p%8L0oso-qajMGEAD6M^&kp~J!QsWc5 zgeK)Xd2%m(XYUF?%_qfX8BV*fkdKG-iy|0!>hR8(<;)cjxXFQ%Hz_EQ#cVBCPiy;| zJ(c1Lypxd~MM?n*Z4Ks&H)2POWVU2Rj~Fazc)Gw9b8ZHCOCMHxpjyV^)<&qU`0C zKVcFTYyH+#aP?PU_SOM=&C#xN;YEq~*J=cO&VBBN(+W`bC3+qJ2Puv7U24h|Ks9=% zNnqxm#R@J|HmlQ?dk`_7f9t2(Kl+pALY+}fm`l*z3*~*974fCAETpG^65cu@5WMK! z$8h%nU!2yHI}Bjl46}2eg|V6tR;k~8#)`Dp2+IZ%B8Y$KBI3h=1csOM8JqM`-QfoBbAMRGqE{>2EzECw8Qcqz$Nuz zSkDH>ml1lKo+B}drq6XbQE=VV+)xL>TU~@}TFN26GK6q|6kQ{p( zGxk;lb+Cvo$3YC*(IG~srQdwuan*LzO)dutU9tFg0jz7Fx%<0z_^i$j%Y4PhbdmG)mjrm4Xoe_{RDx*0n*>AL$UD9No zwNyXD0aB=rx;grP2>}*AY@YfwEEf}Oa^hfz*fEWP{FsK#wbSP;*a)sirz6`(dSHxe zko8yZj8VlwLO^ZRV&B6qIFBCQD!{V8G;%az*N`=EY?}4Uq9S9JF8~5ztU<`jXmPI7 ztUrfMg!e`K%q_P-XO<1#BUACTtk)v|26*aTCfneLT+HRe_r!P}4Qa4}KIeh#6d@a~ zEhNKq5keA-F%ZkcRBszHaPGq&HHPvV6(34!J?SQ_O`2+6BB$-i@sh)AtiLdGajTP~ zm&b!57Z=v)mXE0(uhQZtnIv0o8NUSib28hKcV!HE(so=NB(I%)bP#l3ztO@y8~ow) z#d|(I+VmRp71GdWQlky~t((&WN$q|%&&RSEDicQng@4G0g`(ACFDwV2mtM?>lqz-D z9bSU0xDXBV$&V#$D7t8?{_7LF#J2K$2qujIllg`$f&t@mR`OWU{$&{)njH6OdWn2M z_ApP%xb4ug#=}hSsWmA)(^w!a9p;n7iQOK!@8{K0q?guCAJhOzi!Q_pBWsV z&Tm5iOqz^=CCuBA7+$8s%CTi^W@FlBQD>YnH4?9Fjcv30qBAb>$KWtY8>7IV!Yy?E z*vj7El~H&9Nll3GRUK<|a<=-upXL-?Bbz1gt3vZi&tEjoR*Nk18x`6FmL~lWGuX6CRy8Khjc%Z zFI+yeU~3Yc`fp<%A&O)>mvIMdrz-C(XoAd5K~lf==A>Qyy#L|2py{J#4AE8*Um|m} zPb-9M>rZ!v>P4+{d_gzem3(I>4r2G;X#__4Az6pnJ7Rax`G?bkkAn?a0^R;WzJti% zMVp5tU>o)+9^ha(VyWdpZ|w!~u8Sb5k><ZaVBJPQc<%s!w zLKLWY__J1D?A5uK@Y6tr44$!- zdIyKiSM;xprRof&l6rp|~x2>jRe+BnnOz4*f!@3z0)ovcFsqYKdJLLkw5 z@UruS-%_e%P(|7e^YS+F`|OGZcXQPp7CD<7ULHENt%^NaBdsnW;PZ*t(_a%(J*{u- zuH1Hxk~t5(N%s2Mlx}s2;AP(JCzQ9(>RwZG^nP_PGu8oy+f}8~Qs(meY+kf~+cnA@ zwdn1d;r!WnapewX2L}DnC8AY=3f85JL1Nr!V`clYe7#k8$q5yaf(cNHi3|50^{5%0|poca}^R zX@;xgyLhb|+I2Mu;KK7zp=Lu9nn{tTb1q5qQPW~|hAs^L9te%q*YQ%S!Vb}m37qso zMn(#S^QmiGBr_>8)uEXNV6oq6P2~qHY0ZPH+DkNuJ0?2=bw((@Tv!eb>^;y9eB|Y- zL!ldm>u@%Ewu90zNT2E8xyn3aFVC$t-7i_BCCVBGQ&!DicHh%I-Z5 z=xl7@srS;Ok2Cb{a90(PpH1Kq?%papY!0mND}P3a28{qDg-j{^WxQ^F^Q*!M<13_g z7&Y#^5G^Nx9eY~TL!r%7vycF1Z6kRj0b>@EEatx&=<93{Q~0}&Kv&K zyW6vTw?&^&ulJWxgGKHzgS2Ju*Cs~-^Oat5^M=movt6k=YgYb=_WwMrf~63mcUV&E zqA^X|_%Ji^R_g?o!D2MA?A9Ku^(;5V{dFvt=*+ku>uK*~Lf`@ReV=u}(d9^1i%Ys1 z|F5Q;Se`)Y!Sa5^v9#B-*istUz7{@uZ&j6cjxJs%-uHYdysZ?|@TOIx z_H^1nV{qBpY6ORs{KiHGt%^f+%;Iv#U-qOb9Z){VGNe(*+~w)l?ybJT${gSz5`FAF zekz$c^9**8WZL8w0g{RTj19zL;8WPsOu1L=gg}LZkInNTPvhRBPE1PU2B3itK1509 zgjX6^UoB^Ew4q7lF>IyX)IAyk^EbsxM*-d$R0X^3z@fGr`>zqQ+Xq7P1x=b_>efef%5(`$*x8v?bM8#agvTu_DxQM5F#NcI`B*~Kb7XUhRA22Pw0jqsvR5H}H{R8(8-O^|D|IUF#-03QSi zhPUxWP>07+Gjv;LeI@Z8<%|DE4f3OaeaA)U2DF8r5%4U<+2<*00#Jq<`}I8RVU3}F zK&aK;7!*N`z_1Vw+;r)U%j1S@$-P5)s16Gg2D|}LfMI)>2Ia2?U&bN<)e<`Et0mJL zrT6op$;RX?0!qpxtXgEvQtYEm0q}MZfy$BIUKbw#7y{~7|&<>P4eT^>3 zZkmHCAT$Pl`v|-bL!B@fG)v3ko)*)rRw-oNbWyquhmgLe`8rR#fTs|3!*>Yz>Y7As z&~o(oWdV=X)lDOD*q=E37QA{kYg>Fbs2_C$aSDoMlpg$Lx-OhjDsQ%T775(g%U|$V zmPv1D3oHr##YM<-(v18@IXVpRN2m*ir==M(Hw~7)2av-9qE#=8715~ws@s#61?jrJ z>As2a#e*(9K%}45GK2ISkk=!!!EHOR8LiqWl_psGiKM}3+tMT#$bv8@SP*mnQvzLXf`)eB;Sa)RBVclqC{4 znyBc&!k{3l4%j+}icX!Or;X0-yvI&Ml3IkkSuOc$UT0hF&Au)t|LUYXd$zuDqsK)C zbO?ZO?;7xxuY1x06XxsC1tq(v^T>*l76$mAKv5L88sNhL9ldD)$TLO;Hx`ccUje2g zXM4h*zJfC(u|j99xm3@&(4n*7nhp5^!nFI#4-00kReT)4G|ClU73XEMcvNl}OCc!~ z@H77!&A5`afI7qe*}K5?ArZEM20Dn1$Z^ zHbj^_+hh?)^LeB0#+?^OEOsi<#8@s04 zB=rObD|0-t$6-2TKmE$X=FdvUDSrg=@jpO|S$(=ESMt<*N^DOdEbmBZ+u2q68M^+$ zG1+r=>7~kBY^`6@(zakF#?!cdq7P% zGOsu;6dB0z-{_kTw&;LGxouT0_0T(x+yx{_9f%KC-^Lrds4#}VCXM1u_N!WXqU38+ zoi-~KDG1PKdiF;%{e>LdFfDD5N_0v-%wj6DsFbV) zA?(pRlK3);%7c4UgyZAJArIr?aKBL&@gN`La6pUip2@j7GvQtINg|m)ko{i*Y#eNY zkT%&?(SY^~Csc|{%-jNO^TKHyPMdZ;X51!H804BfkJSnT!XrK<#%UK3&MoRDvh1#> zr5iYTm$fMR&Z?^O8XghRj`aJ^*U7dHM&2kH;-dH$EQc?77cQPg8$%$zyR} z%3<-N<~zIEhUI!G29mmV6Fdw%1ypoq!|X`pv(^I5D(C2$N_lJD)u6>4}`}F(dp6d}uHbdI)wXb!FO_^+*!0E}`wojkc9a`xb z+&x`ZjfjAP;oIw;vjg7GSFqx)>Bnu_uw8T>NOB6=s3xmB7^6r0f<6kE;riKolgmaF zd4xZw$|U9J{3a~2r~tAtx_ee2O=-B!ZA{=qL+wnP$D?~c2^!70wGD&rY2|zdlMp#M z@J;oo+r0b~b7#rl>e)+Kb@`^9u_d`eb$C-AAyEA)IkixC`f&AvJ?`;+s}CQP?84dv z^1#nSNJ=#^*0DB0{-`fT>lw;$O#ct7T3U7NLm)4H;M-EAsB@~oas5ozLaiGoP@T!N z`uRSf$@hT(PzwXn9k0CyRSGUL#4)2x928J%_>Gx_q5QZGC+vJ2o|?l;7v#*e+k5_@ zRYWc(HNK`TFZ+WDztS#FeosYvsBIt%N)J#viHh+C6G9p zCTacb@%vekfecHovfz_U61X*Tx^hr_+G_jX_%h!-;$89(2D&C&YFnhhOR3rX0E=4dDP=r#kVC@uOH*22e z#D`#jCwPXJMH2@}&UU~FSmN7m6u5SNhN8{dm)tW_j*e%PH*@o){%i?)oLUK)aTHf6 zWK1m&Ejmi@#VzUug6n-J^?!|(MD?awacR?~eriaqP*!?3DJ@l$)Agy3v-s-U(A%Vv z+Eu-p*afcDo)!?aVEOdWh-a0I1nb{8I@ew*wYf|k*T8V8G;>Vtd2cq9Cwc!7>PvrlscX~c zFY`xKb&bufygWE%M_RRw20iqF3=BT*aZBjOq4J{&y(46M1fY16diohguKe#_z7g5b zk#aI@a1(dvcd?|$Oy&1GYX2 z+o=W*BLZ!7Uc0{V3nabG(8r?l-gaP}S@R~1?sr#Q1ens@c+tu$&LU_<_xgCeDbHha zA+N;}k5DQMEJK&mIo~p~s2oyXx^nZ(`JFJ{f@QXjy@~xR`+T*?F zMn9Js3~I$SDHVYKY|rmtpbBI1`Nc)+rq`?RDuaiE%{iSn-yW2GjataXk)bS_L5BUv z52-CTpFFx+uw6bK@I2No(Ua}J)bu)qJ(R4NmV+<((opsZ*CeJJ0}xa$5Y@KFo!Ps@ zQIjX#DzFLOlcs*{*i%o%Jl`L7P+Y&!eyWgxKH}<8o`i(8LAaGJ4NnfBLJ_p4}MSHwf!$4iFPR}Pr72xMO-SbE40OS(1h|oDbM2~ z0cbc{Jf7zeZ2-6k4V;HFi_4T)RGL;NDvKNvy2Cd9omcJvu6&6 zB28~{k3vv5NOj;~(np=4P^C&G7;qnBzKJ7xz3OOL03~dy$&5cWV|by0O3`Gz$#evH zy^CxMW2<;N49{%yysU2J+^l%SOmg!b5+mG)>h#jS6u zfjez3{CeM2L_^30T+J+~t;Y>NtZ~xt?a8@WAcF)`j5egTmah`!Tct6xWcBumhe`*L zo;o)Z6{5+E>^Yba|LPA%hB9NF#ivdlHN{O=chhXf{XmqFc%<(ao03RJ;Nkg8k};FVQEa;+^8)z z7FOx_g4O$?aPp4o&TqA5rFx)>t{xz3FSN75pyNgX5Pz;`REU}in+7d}Y>3YzG*DgslhpA_ z{dzMqA+avN+-317jz+u{ax-E+E}~D)eLh@b*W?Ee|Fo*|*ChU6@%H55h+D+JiFEm> z2<5;~vuk6j)N@qzXs=ikdk2_jlJ+9i1_l#|u-*Lcz)1&5Ap(NcxPYXIn!I(LbdPld z+QX+lxzqVh8P7gW)$E#NRrxx*9fw`7W%R{p9E}(cx)diVn$ZzvR3Esv#QdkJGkHva zW0}=d_jFBazkzLnC-o~#%l+BTWH~3))E~jxqW8S_kKCm>8@g`ItV=BB$8=NAJIPJU z@TL@#?m8NjiH-Yx_%`%>z-R`JWSU)hZjx+&zqziEDZ>F}e}Czyet<7aDN}RG;Qwpy z+TWSp$Y6xRxDi{mr3R_EILD+5eqXh z%%g_Qa1zON$z?4O8#8NkzWf#Ehx7RhUZ3~#{=DAL`~7*|ub1J6VpX`hN4|i@J~oC z8f(1Ex~GfmujN2!#1LdD{k=pd-C{kfF6_+R_HVb6y71nx&?pxcjfFSBCi@)Pe7mfx z5|kaz{LCKX>@+8xQ#NI82pPM>%;E{MLKSulB?4t1+mI$CFJ{CIhKZ`Rp>opw5<#|9;Q4>Z|)m|O9>)BYS z3Y2yjOT+4_IbOn9@Vt_;ID^EKC57@>`g%b&vlrrrYU%hpsAZDm9Hku z*MALa4dCwHYoq856*sYXTlF3Lfvy6*U!-|q+>W^@$)y= zPG*ms9ZFLzL5ghr>Z6InsuiKjt$RMc;F;$GhCeF_LDGglsKi&3Vh*Qcm8PsTPcSj1 zLLgNIqYaFVI@X-H`}I1yMP4-bSNsID$7u86yJwb`>g3eNy^W@MyK{l#cB&UgJ+|EO zVN$G9Xtb|J;MFcW44c%@&Q`7wV31B$x9B9Av% zXm2#;+|=lZs!xoCHhp0hhdT66xm!QB*)vA(w?g^GPM;$Qt7;8SLIslxQ+{{+TM!eh zqRV~5i04D~xG!BEwMQ-Lv0F%F?M?~1@Dwq;5-TXSI#Buj9iuNR+(D;X7%{XoDhY4c zLS1}0O#nT_wF>f`t!E@&vE)$d=i(-Y<}b=(%q1)^jqyXS2jz)EGb zrfo!XQ2##1Z<2``g+vzL_l~J0pM(emV zcg=7ui=oAs`n6Ws1rTcTr!AGv`nb^2ZzhZ7F|$>$8u-iz08HuyMkvM_txFPq!ka-Q zigL6c?{G%2BYO!8Lus(uw4x(syiPcS0Vnog{vE(jL}7XmViL9~LUEq|ZkYpu8|BMQ zAl>Xiu~ zn3`?)we?U=Lw{RNJcWP2%*SuTnX7K80f74R1~Ip@Zgh1b;%^myc4VUOcF(vVEpwWi zd)k$GigqF8V3LqAJVu*j@qyYSk2Yx8^86?iudZ&}J_GSCrrA_9u6j}0f?Y{VCY6%N zl|pjeUApxU=X_v<)I1OwkX7@Zx~W_`1!&*?fBu^h=*&!3I3I~VSC}Y!wSy30>ughJ HedGSW=zn+i literal 0 HcmV?d00001 diff --git a/apps/web/public/pools-sitemap.xml b/apps/web/public/pools-sitemap.xml index 08339144e0f..03d6f39b07c 100644 --- a/apps/web/public/pools-sitemap.xml +++ b/apps/web/public/pools-sitemap.xml @@ -13130,4 +13130,24 @@ 2025-10-17T22:04:33.647Z 0.8 + + https://app.uniswap.org/explore/pools/ethereum/0xa3ccaf08a54cf31649f91ae1570a0720c8d4eb1e + 2025-10-24T19:06:27.459Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xd13040d4fe917ee704158cfcb3338dcd2838b245 + 2025-10-24T19:06:27.459Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x75c5fbf77c1cd517544487aca4cc41e1ad95aced + 2025-10-24T19:06:27.459Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x8c5a402ede3a33998604c8ba5fe6510896cb3821 + 2025-10-24T19:06:27.459Z + 0.8 + \ No newline at end of file diff --git a/apps/web/public/tokens-sitemap.xml b/apps/web/public/tokens-sitemap.xml index a865ac527e2..cc16efb4f28 100644 --- a/apps/web/public/tokens-sitemap.xml +++ b/apps/web/public/tokens-sitemap.xml @@ -11115,4 +11115,94 @@ 2025-10-17T22:04:33.647Z 0.8 + + https://app.uniswap.org/explore/tokens/solana/METvsvVRapdj9cFLzq4Tr43xK4tAjQfwX76z3n6mWQL + 2025-10-24T19:06:27.459Z + 0.8 + + + https://app.uniswap.org/explore/tokens/solana/Dfh5DzRgSvvCFDoYc2ciTkMrbDfRKybA4SoFbPmApump + 2025-10-24T19:06:27.459Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x815269d17c10f0f3df7249370e0c1b9efe781aa8 + 2025-10-24T19:06:27.459Z + 0.8 + + + https://app.uniswap.org/explore/tokens/solana/SarosY6Vscao718M4A778z4CGtvcwcGef5M9MEH1LGL + 2025-10-24T19:06:27.459Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x93d6afa0e6f11f4f7e9521ec6243f839526af7a6 + 2025-10-24T19:06:27.459Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x4c9027e10c5271efca82379d3123917ae3f2374e + 2025-10-24T19:06:27.459Z + 0.8 + + + https://app.uniswap.org/explore/tokens/solana/USDSwr9ApdHk5bvJKMjzff41FfuX8bSxdKcR81vTwcA + 2025-10-24T19:06:27.459Z + 0.8 + + + https://app.uniswap.org/explore/tokens/solana/SW1TCHLmRGTfW5xZknqQdpdarB8PD95sJYWpNp9TbFx + 2025-10-24T19:06:27.459Z + 0.8 + + + https://app.uniswap.org/explore/tokens/solana/3wPQhXYqy861Nhoc4bahtpf7G3e89XCLfZ67ptEfZUSA + 2025-10-24T19:06:27.459Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xdcaa5e062b2be18e52ea6ed7ba232538621ddc10 + 2025-10-24T19:06:27.459Z + 0.8 + + + https://app.uniswap.org/explore/tokens/solana/METAwkXcqyXKy1AtsSgJ8JiUHwGCafnZL38n3vYmeta + 2025-10-24T19:06:27.459Z + 0.8 + + + https://app.uniswap.org/explore/tokens/solana/oreoU2P8bN6jkk3jbaiVxYnG1dCXcYxwhwyK9jSybcp + 2025-10-24T19:06:27.459Z + 0.8 + + + https://app.uniswap.org/explore/tokens/solana/6nR8wBnfsmXfcdDr1hovJKjvFQxNSidN6XFyfAFZpump + 2025-10-24T19:06:27.459Z + 0.8 + + + https://app.uniswap.org/explore/tokens/solana/AjPzK6Sf1G27jFkFe4HViSNqMxa3JLE4D1fm6Pzouq2q + 2025-10-24T19:06:27.459Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x0a8d6c86e1bce73fe4d0bd531e1a567306836ea5 + 2025-10-24T19:06:27.459Z + 0.8 + + + https://app.uniswap.org/explore/tokens/solana/JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN + 2025-10-24T19:06:27.459Z + 0.8 + + + https://app.uniswap.org/explore/tokens/solana/pSo1f9nQXWgXibFtKf7NWYxb5enAM4qfP6UJSiXRQfL + 2025-10-24T19:06:27.459Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xf245964bd0a73128e10c4f7c96d0664ea2e436d8 + 2025-10-24T19:06:27.459Z + 0.8 + \ No newline at end of file diff --git a/apps/web/src/appGraphql/data/util.tsx b/apps/web/src/appGraphql/data/util.tsx index 18e459e39c1..a7ffb3e9d86 100644 --- a/apps/web/src/appGraphql/data/util.tsx +++ b/apps/web/src/appGraphql/data/util.tsx @@ -189,6 +189,12 @@ const PROTOCOL_META: { [source in GraphQLApi.PriceSource]: ProtocolMeta } = { color: '$chain_137', gradient: { start: 'rgba(96, 123, 238, 0.20)', end: 'rgba(55, 70, 136, 0.00)' }, }, + [GraphQLApi.PriceSource.External]: { + // TODO (LP-350): Remove this since this protocol chart does not exist anymore + name: 'external', + color: '$neutral1', + gradient: { start: 'rgba(252, 116, 254, 0.20)', end: 'rgba(252, 116, 254, 0.00)' }, + }, /* [GraphQLApi.PriceSource.UniswapX]: { name: 'UniswapX', color: purple } */ } diff --git a/apps/web/src/assets/images/portfolio-page-promo/dark.svg b/apps/web/src/assets/images/portfolio-page-promo/dark.svg new file mode 100644 index 00000000000..1ae6e3cb76e --- /dev/null +++ b/apps/web/src/assets/images/portfolio-page-promo/dark.svg @@ -0,0 +1,778 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/src/assets/images/portfolio-page-promo/light.svg b/apps/web/src/assets/images/portfolio-page-promo/light.svg new file mode 100644 index 00000000000..8e9c0579fe4 --- /dev/null +++ b/apps/web/src/assets/images/portfolio-page-promo/light.svg @@ -0,0 +1,778 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/src/assets/svg/Emblem/A.svg b/apps/web/src/assets/svg/Emblem/A.svg new file mode 100644 index 00000000000..46c5ecdf931 --- /dev/null +++ b/apps/web/src/assets/svg/Emblem/A.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/web/src/assets/svg/Emblem/B.svg b/apps/web/src/assets/svg/Emblem/B.svg new file mode 100644 index 00000000000..9ba0cad2077 --- /dev/null +++ b/apps/web/src/assets/svg/Emblem/B.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/src/assets/svg/Emblem/C.svg b/apps/web/src/assets/svg/Emblem/C.svg new file mode 100644 index 00000000000..df525ee3977 --- /dev/null +++ b/apps/web/src/assets/svg/Emblem/C.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/apps/web/src/assets/svg/Emblem/D.svg b/apps/web/src/assets/svg/Emblem/D.svg new file mode 100644 index 00000000000..6673c60e7b6 --- /dev/null +++ b/apps/web/src/assets/svg/Emblem/D.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/web/src/assets/svg/Emblem/E.svg b/apps/web/src/assets/svg/Emblem/E.svg new file mode 100644 index 00000000000..f1d262aa1fd --- /dev/null +++ b/apps/web/src/assets/svg/Emblem/E.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/src/assets/svg/Emblem/F.svg b/apps/web/src/assets/svg/Emblem/F.svg new file mode 100644 index 00000000000..f7f9944dbfa --- /dev/null +++ b/apps/web/src/assets/svg/Emblem/F.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/src/assets/svg/Emblem/G.svg b/apps/web/src/assets/svg/Emblem/G.svg new file mode 100644 index 00000000000..44e41f65357 --- /dev/null +++ b/apps/web/src/assets/svg/Emblem/G.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/src/assets/svg/Emblem/default.svg b/apps/web/src/assets/svg/Emblem/default.svg new file mode 100644 index 00000000000..1839d363511 --- /dev/null +++ b/apps/web/src/assets/svg/Emblem/default.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/src/components/AccountDrawer/AuthenticatedHeader.tsx b/apps/web/src/components/AccountDrawer/AuthenticatedHeader.tsx index 638bf5898ad..72bdd2ef2ee 100644 --- a/apps/web/src/components/AccountDrawer/AuthenticatedHeader.tsx +++ b/apps/web/src/components/AccountDrawer/AuthenticatedHeader.tsx @@ -1,5 +1,6 @@ import { NetworkStatus } from '@apollo/client' import { CurrencyAmount, Token } from '@uniswap/sdk-core' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { MultiBlockchainAddressDisplay } from 'components/AccountDetails/MultiBlockchainAddressDisplay' import { ActionTile } from 'components/AccountDrawer/ActionTile' import { DisconnectButton } from 'components/AccountDrawer/DisconnectButton' @@ -38,8 +39,6 @@ import { UniverseChainId } from 'uniswap/src/features/chains/types' import { usePortfolioTotalValue } from 'uniswap/src/features/dataApi/balances/balancesRest' import { FiatCurrency } from 'uniswap/src/features/fiatCurrency/constants' import { useAppFiatCurrency, useAppFiatCurrencyInfo } from 'uniswap/src/features/fiatCurrency/hooks' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext' import { Platform } from 'uniswap/src/features/platforms/types/Platform' import { useHasAccountMismatchOnAnyChain } from 'uniswap/src/features/smartWallet/mismatch/hooks' diff --git a/apps/web/src/components/AccountDrawer/DisconnectButton.tsx b/apps/web/src/components/AccountDrawer/DisconnectButton.tsx index 4cd815d1f91..0e23c16c916 100644 --- a/apps/web/src/components/AccountDrawer/DisconnectButton.tsx +++ b/apps/web/src/components/AccountDrawer/DisconnectButton.tsx @@ -1,3 +1,4 @@ +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { useAccountDrawer } from 'components/AccountDrawer/MiniPortfolio/hooks' import { MenuStateVariant, useSetMenu } from 'components/AccountDrawer/menuState' import { Power } from 'components/Icons/Power' @@ -14,8 +15,6 @@ import { PlusCircle } from 'ui/src/components/icons/PlusCircle' import { SwitchArrows } from 'ui/src/components/icons/SwitchArrows' import { AppTFunction } from 'ui/src/i18n/types' import { CONNECTION_PROVIDER_IDS } from 'uniswap/src/constants/web3' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { Platform } from 'uniswap/src/features/platforms/types/Platform' import { setIsTestnetModeEnabled } from 'uniswap/src/features/settings/slice' import { ElementName } from 'uniswap/src/features/telemetry/constants' diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/ActivityTab.anvil.e2e.test.ts b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/ActivityTab.anvil.e2e.test.ts index 413b3f15890..432c2f5f253 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/ActivityTab.anvil.e2e.test.ts +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/ActivityTab.anvil.e2e.test.ts @@ -19,6 +19,7 @@ test.describe('ActivityTab activity history', () => { }) await stubTradingApiEndpoint({ page, endpoint: uniswapUrls.tradingApiPaths.swap }) + await stubTradingApiEndpoint({ page, endpoint: uniswapUrls.tradingApiPaths.quote }) await graphql.intercept('ActivityWeb', Mocks.Account.activity_history) await page.goto(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`) diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/ExtensionDeeplinks.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/ExtensionDeeplinks.tsx index f163e30df46..34d31b6c4dc 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/ExtensionDeeplinks.tsx +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/ExtensionDeeplinks.tsx @@ -1,4 +1,4 @@ -import { PositionStatus } from '@uniswap/client-pools/dist/pools/v1/types_pb' +import { PositionStatus } from '@uniswap/client-data-api/dist/data/v1/poolTypes_pb' import { useOpenLimitOrders, usePendingActivity } from 'components/AccountDrawer/MiniPortfolio/Activity/hooks' import { useAccountDrawer } from 'components/AccountDrawer/MiniPortfolio/hooks' import { MenuStateVariant, useSetMenu } from 'components/AccountDrawer/menuState' diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Pools/PoolsTab.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Pools/PoolsTab.tsx index d4bb8b966c4..5dbd6c74049 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Pools/PoolsTab.tsx +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Pools/PoolsTab.tsx @@ -1,4 +1,4 @@ -import { PositionStatus, ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' +import { PositionStatus, ProtocolVersion } from '@uniswap/client-data-api/dist/data/v1/poolTypes_pb' import { ExpandoRow } from 'components/AccountDrawer/MiniPortfolio/ExpandoRow' import { useAccountDrawer } from 'components/AccountDrawer/MiniPortfolio/hooks' import { EmptyPools } from 'components/AccountDrawer/MiniPortfolio/Pools/EmptyPools' diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/PortfolioRow.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/PortfolioRow.tsx index 7c5c73a38ee..404fd569423 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/PortfolioRow.tsx +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/PortfolioRow.tsx @@ -1,5 +1,5 @@ -import { LoadingBubble } from 'components/Tokens/loading' -import { Flex, FlexProps } from 'ui/src' +import { TextLoader } from 'components/Liquidity/Loader' +import { Circle, Flex, FlexProps, Shine } from 'ui/src' const PortfolioRowWrapper = ({ children, className, ...rest }: FlexProps) => ( - - - - - - - {shrinkRight ? ( - - ) : ( - <> - - - - )} + + + + + + + + + + + + + + + + + + + + + - + ) } -export function PortfolioSkeleton({ shrinkRight = false }: { shrinkRight?: boolean }) { +export function PortfolioSkeleton() { return ( - <> + {Array.from({ length: 5 }).map((_, i) => ( - + ))} - + ) } diff --git a/apps/web/src/components/ActivityTable/ActivityAddressCell.tsx b/apps/web/src/components/ActivityTable/ActivityAddressCell.tsx new file mode 100644 index 00000000000..b47c5acf5cd --- /dev/null +++ b/apps/web/src/components/ActivityTable/ActivityAddressCell.tsx @@ -0,0 +1,27 @@ +import { AddressWithAvatar } from 'components/ActivityTable/AddressWithAvatar' +import { buildActivityRowFragments } from 'components/ActivityTable/registry' +import { Flex } from 'ui/src' +import { ArrowRight } from 'ui/src/components/icons/ArrowRight' +import { TransactionDetails } from 'uniswap/src/features/transactions/types/transactionDetails' +import { getValidAddress } from 'uniswap/src/utils/addresses' + +interface ActivityAddressCellProps { + transaction: TransactionDetails +} + +export function ActivityAddressCell({ transaction }: ActivityAddressCellProps) { + const { counterparty } = buildActivityRowFragments(transaction) + + // Use counterparty from adapter if available, otherwise fall back to from address + const rawAddress = counterparty ?? transaction.from + const otherPartyAddress = rawAddress ? getValidAddress({ address: rawAddress, chainId: transaction.chainId }) : null + + return ( + + {otherPartyAddress && } + + + + + ) +} diff --git a/apps/web/src/components/ActivityTable/ActivityAmountCell.tsx b/apps/web/src/components/ActivityTable/ActivityAmountCell.tsx new file mode 100644 index 00000000000..b3cddbac36b --- /dev/null +++ b/apps/web/src/components/ActivityTable/ActivityAmountCell.tsx @@ -0,0 +1,255 @@ +import { buildActivityRowFragments } from 'components/ActivityTable/registry' +import { TokenAmountDisplay } from 'components/ActivityTable/TokenAmountDisplay' +import { useTranslation } from 'react-i18next' +import { Flex, Text } from 'ui/src' +import { ArrowRight } from 'ui/src/components/icons/ArrowRight' +import { useFormattedCurrencyAmountAndUSDValue } from 'uniswap/src/components/activity/hooks/useFormattedCurrencyAmountAndUSDValue' +import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' +import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext' +import { + useCurrencyInfo, + useNativeCurrencyInfo, + useWrappedNativeCurrencyInfo, +} from 'uniswap/src/features/tokens/useCurrencyInfo' +import { TransactionDetails } from 'uniswap/src/features/transactions/types/transactionDetails' +import { getSymbolDisplayText } from 'uniswap/src/utils/currency' +import { NumberType } from 'utilities/src/format/types' + +interface ActivityAmountCellProps { + transaction: TransactionDetails +} + +function EmptyCell() { + return ( + + — + + ) +} + +interface DualTokenLayoutProps { + inputCurrency: CurrencyInfo | null | undefined + outputCurrency: CurrencyInfo | null | undefined + inputFormattedAmount: string | null + outputFormattedAmount: string | null + inputUsdValue: string | null + outputUsdValue: string | null + separator?: React.ReactNode +} + +function Separator({ children }: { children: React.ReactNode }) { + return ( + + {typeof children === 'string' ? ( + + {children} + + ) : ( + children + )} + + ) +} + +function DualTokenLayout({ + inputCurrency, + outputCurrency, + inputFormattedAmount, + outputFormattedAmount, + inputUsdValue, + outputUsdValue, + separator = , +}: DualTokenLayoutProps) { + return ( + + + {separator} + + + ) +} + +function formatAmountWithSymbol(amount: string | undefined, symbol: string | undefined): string | null { + return amount ? `${amount}${getSymbolDisplayText(symbol)}` : null +} + +function getUsdValue(value: string | undefined): string | null { + return value !== '-' ? (value ?? null) : null +} + +export function ActivityAmountCell({ transaction }: ActivityAmountCellProps) { + const formatter = useLocalizationContext() + const { t } = useTranslation() + const { chainId } = transaction + const { amount } = buildActivityRowFragments(transaction) + + // Hook up currency info based on amount model + const inputCurrencyInfo = useCurrencyInfo(amount?.kind === 'pair' ? amount.inputCurrencyId : undefined) + const outputCurrencyInfo = useCurrencyInfo(amount?.kind === 'pair' ? amount.outputCurrencyId : undefined) + const singleCurrencyInfo = useCurrencyInfo( + amount?.kind === 'single' || amount?.kind === 'approve' ? amount.currencyId : undefined, + ) + const currency0Info = useCurrencyInfo(amount?.kind === 'liquidity-pair' ? amount.currency0Id : undefined) + const currency1Info = useCurrencyInfo(amount?.kind === 'liquidity-pair' ? amount.currency1Id : undefined) + + const nativeCurrencyInfo = useNativeCurrencyInfo(chainId) + const wrappedCurrencyInfo = useWrappedNativeCurrencyInfo(chainId) + + // Format amounts based on kind + const inputFormattedData = useFormattedCurrencyAmountAndUSDValue({ + currency: inputCurrencyInfo?.currency, + currencyAmountRaw: amount?.kind === 'pair' ? (amount.inputAmountRaw ?? '') : '', + formatter, + isApproximateAmount: false, + }) + + const outputFormattedData = useFormattedCurrencyAmountAndUSDValue({ + currency: outputCurrencyInfo?.currency, + currencyAmountRaw: amount?.kind === 'pair' ? (amount.outputAmountRaw ?? '') : '', + formatter, + isApproximateAmount: false, + }) + + const singleFormattedData = useFormattedCurrencyAmountAndUSDValue({ + currency: singleCurrencyInfo?.currency, + currencyAmountRaw: amount?.kind === 'single' ? (amount.amountRaw ?? '') : '', + formatter, + isApproximateAmount: false, + }) + + const wrapAmountRaw = amount?.kind === 'wrap' ? (amount.amountRaw ?? '') : '' + const wrapInputCurrency = amount?.kind === 'wrap' && amount.unwrapped ? wrappedCurrencyInfo : nativeCurrencyInfo + const wrapOutputCurrency = amount?.kind === 'wrap' && amount.unwrapped ? nativeCurrencyInfo : wrappedCurrencyInfo + + const wrapInputFormattedData = useFormattedCurrencyAmountAndUSDValue({ + currency: wrapInputCurrency?.currency, + currencyAmountRaw: wrapAmountRaw, + formatter, + isApproximateAmount: false, + }) + + const wrapOutputFormattedData = useFormattedCurrencyAmountAndUSDValue({ + currency: wrapOutputCurrency?.currency, + currencyAmountRaw: wrapAmountRaw, + formatter, + isApproximateAmount: false, + }) + + const currency0FormattedData = useFormattedCurrencyAmountAndUSDValue({ + currency: currency0Info?.currency, + currencyAmountRaw: amount?.kind === 'liquidity-pair' ? amount.currency0AmountRaw : '', + formatter, + isApproximateAmount: false, + }) + + const currency1FormattedData = useFormattedCurrencyAmountAndUSDValue({ + currency: currency1Info?.currency, + currencyAmountRaw: amount?.kind === 'liquidity-pair' ? (amount.currency1AmountRaw ?? '') : '', + formatter, + isApproximateAmount: false, + }) + + if (!amount) { + return + } + + // Guard against missing currency data before formatting + if (amount.kind === 'pair' && (!inputCurrencyInfo || !outputCurrencyInfo)) { + return + } + + if (amount.kind === 'liquidity-pair' && (!currency0Info || !currency1Info)) { + return + } + + switch (amount.kind) { + case 'pair': { + // Dual token layout for swaps and bridges: Token1 → Token2 + return ( + + ) + } + + case 'approve': { + // Single token layout for approvals + let formattedAmount: string | null = null + + if (singleCurrencyInfo && amount.approvalAmount !== undefined) { + const amountText = + amount.approvalAmount === 'INF' + ? t('transaction.amount.unlimited') + : amount.approvalAmount && amount.approvalAmount !== '0.0' + ? formatter.formatNumberOrString({ value: amount.approvalAmount, type: NumberType.TokenNonTx }) + : '' + + formattedAmount = `${amountText ? amountText + ' ' : ''}${getSymbolDisplayText(singleCurrencyInfo.currency.symbol) ?? ''}` + } + + return + } + + case 'wrap': { + // Dual token layout for wraps: ETH ↔ WETH + return ( + + ) + } + + case 'single': { + // Single token layout for transfers + return ( + + ) + } + + case 'liquidity-pair': { + // Dual token layout for liquidity: Token0 and Token1 + return ( + + ) + } + } +} diff --git a/apps/web/src/components/ActivityTable/ActivityTable.tsx b/apps/web/src/components/ActivityTable/ActivityTable.tsx new file mode 100644 index 00000000000..97b62e13d1d --- /dev/null +++ b/apps/web/src/components/ActivityTable/ActivityTable.tsx @@ -0,0 +1,123 @@ +import { createColumnHelper, Row } from '@tanstack/react-table' +import { ActivityAddressCell } from 'components/ActivityTable/ActivityAddressCell' +import { ActivityAmountCell } from 'components/ActivityTable/ActivityAmountCell' +import { TimeCell } from 'components/ActivityTable/TimeCell' +import { TransactionTypeCell } from 'components/ActivityTable/TransactionTypeCell' +import { Table } from 'components/Table' +import { Cell } from 'components/Table/Cell' +import { HeaderCell } from 'components/Table/styled' +import { memo, useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { Text } from 'ui/src' +import { TransactionDetails } from 'uniswap/src/features/transactions/types/transactionDetails' + +interface ActivityTableProps { + data: TransactionDetails[] + loading?: boolean + error?: boolean + rowWrapper?: (row: Row, content: JSX.Element) => JSX.Element +} + +function _ActivityTable({ data, loading = false, error = false, rowWrapper }: ActivityTableProps): JSX.Element { + const { t } = useTranslation() + const columnHelper = useMemo(() => createColumnHelper(), []) + const showLoadingSkeleton = loading || error + + const columns = useMemo( + () => [ + // Time Column + columnHelper.accessor('addedTime', { + header: () => ( + + + {t('portfolio.activity.table.column.time')} + + + ), + cell: (info) => { + if (showLoadingSkeleton) { + return + } + return ( + + + + ) + }, + }), + + // Type Column + columnHelper.accessor((row) => row.typeInfo.type, { + id: 'type', + header: () => ( + + + {t('portfolio.activity.table.column.type')} + + + ), + cell: (info) => { + if (showLoadingSkeleton) { + return + } + return ( + + + + ) + }, + }), + + // Amount Column + columnHelper.display({ + id: 'amount', + header: () => ( + + + {t('portfolio.activity.table.column.amount')} + + + ), + cell: (info) => { + if (showLoadingSkeleton) { + return + } + return ( + + + + ) + }, + minSize: 280, + size: 300, + }), + + // Address Column + columnHelper.display({ + id: 'address', + header: () => ( + + + {t('portfolio.activity.table.column.address')} + + + ), + cell: (info) => { + if (showLoadingSkeleton) { + return + } + return ( + + + + ) + }, + }), + ], + [t, columnHelper, showLoadingSkeleton], + ) + + return +} + +export const ActivityTable = memo(_ActivityTable) diff --git a/apps/web/src/components/ActivityTable/AddressWithAvatar.tsx b/apps/web/src/components/ActivityTable/AddressWithAvatar.tsx new file mode 100644 index 00000000000..4abee3d5de7 --- /dev/null +++ b/apps/web/src/components/ActivityTable/AddressWithAvatar.tsx @@ -0,0 +1,42 @@ +import { Flex, Text } from 'ui/src' +import { Unitag } from 'ui/src/components/icons/Unitag' +import { useUnitagsAddressQuery } from 'uniswap/src/data/apiClients/unitagsApi/useUnitagsAddressQuery' +import { AccountIcon } from 'uniswap/src/features/accounts/AccountIcon' +import { useENSName } from 'uniswap/src/features/ens/api' +import { shortenAddress } from 'utilities/src/addresses' + +interface AddressWithAvatarProps { + address: Address + size?: number + showAvatar?: boolean +} + +export function AddressWithAvatar({ address, size = 20, showAvatar = true }: AddressWithAvatarProps) { + const { data: ENSName } = useENSName(address) + const { data: unitag } = useUnitagsAddressQuery({ + params: address ? { address } : undefined, + }) + const uniswapUsername = unitag?.username + + const displayName = uniswapUsername ?? ENSName ?? shortenAddress({ address }) + const hasUnitag = Boolean(uniswapUsername) + + return ( + + {showAvatar && ( + + )} + + {displayName} + + {hasUnitag && } + + ) +} diff --git a/apps/web/src/components/ActivityTable/TimeCell.tsx b/apps/web/src/components/ActivityTable/TimeCell.tsx new file mode 100644 index 00000000000..f90326cfbb8 --- /dev/null +++ b/apps/web/src/components/ActivityTable/TimeCell.tsx @@ -0,0 +1,15 @@ +import { TableText } from 'components/Table/styled' +import { useFormattedTimeForActivity } from 'uniswap/src/components/activity/hooks/useFormattedTime' + +interface TimeCellProps { + timestamp: number +} + +export function TimeCell({ timestamp }: TimeCellProps) { + const formattedTime = useFormattedTimeForActivity(timestamp) + return ( + + {formattedTime} + + ) +} diff --git a/apps/web/src/components/ActivityTable/TokenAmountDisplay.tsx b/apps/web/src/components/ActivityTable/TokenAmountDisplay.tsx new file mode 100644 index 00000000000..ef04e9384bf --- /dev/null +++ b/apps/web/src/components/ActivityTable/TokenAmountDisplay.tsx @@ -0,0 +1,32 @@ +import { TableText } from 'components/Table/styled' +import { Flex } from 'ui/src' +import { CurrencyLogo } from 'uniswap/src/components/CurrencyLogo/CurrencyLogo' +import { useCurrencyInfo } from 'uniswap/src/features/tokens/useCurrencyInfo' + +interface TokenAmountDisplayProps { + currencyInfo: ReturnType + formattedAmount: string | null + usdValue: string | null +} + +export function TokenAmountDisplay({ currencyInfo, formattedAmount, usdValue }: TokenAmountDisplayProps) { + if (!currencyInfo || !formattedAmount) { + return null + } + + return ( + + + + + {formattedAmount} + + {usdValue && ( + + {usdValue} + + )} + + + ) +} diff --git a/apps/web/src/components/ActivityTable/TransactionTypeCell.tsx b/apps/web/src/components/ActivityTable/TransactionTypeCell.tsx new file mode 100644 index 00000000000..5c3f52d05e7 --- /dev/null +++ b/apps/web/src/components/ActivityTable/TransactionTypeCell.tsx @@ -0,0 +1,30 @@ +import { buildActivityRowFragments } from 'components/ActivityTable/registry' +import { TableText } from 'components/Table/styled' +import { getTransactionTypeFilterOptions } from 'pages/Portfolio/Activity/Filters/utils' +import { useTranslation } from 'react-i18next' +import { Flex } from 'ui/src' +import { TransactionDetails } from 'uniswap/src/features/transactions/types/transactionDetails' + +interface TransactionTypeCellProps { + transaction: TransactionDetails +} + +export function TransactionTypeCell({ transaction }: TransactionTypeCellProps) { + const { t } = useTranslation() + const { typeLabel } = buildActivityRowFragments(transaction) + + // Get the icon from the filter options based on base group + const transactionTypeOptions = getTransactionTypeFilterOptions(t) + const typeOption = typeLabel?.baseGroup ? transactionTypeOptions[typeLabel.baseGroup] : null + const IconComponent = typeOption?.icon + + // Use override label key if provided, otherwise use the base group label + const label = typeLabel?.overrideLabelKey ? t(typeLabel.overrideLabelKey) : (typeOption?.label ?? 'Transaction') + + return ( + + {IconComponent && } + {label} + + ) +} diff --git a/apps/web/src/components/ActivityTable/activityTableModels.ts b/apps/web/src/components/ActivityTable/activityTableModels.ts new file mode 100644 index 00000000000..f1fc9d10af1 --- /dev/null +++ b/apps/web/src/components/ActivityTable/activityTableModels.ts @@ -0,0 +1,61 @@ +/** + * Models for activity table presentation layer. + * These types describe table-ready data from transaction parsers, without formatting or i18n. + * Each adapter returns raw IDs, amounts, addresses, and translation keys. + */ + +/** + * Represents the amount/token data for different transaction types + */ +type ActivityAmountModel = + | { + kind: 'pair' + inputCurrencyId: string + outputCurrencyId: string + inputAmountRaw?: string + outputAmountRaw?: string + } + | { + kind: 'single' + currencyId?: string + amountRaw?: string + } + | { + kind: 'approve' + currencyId?: string + approvalAmount?: string | 'INF' + } + | { + kind: 'wrap' + unwrapped: boolean + amountRaw?: string + } + | { + kind: 'liquidity-pair' + currency0Id: string + currency1Id: string + currency0AmountRaw: string + currency1AmountRaw?: string + } + +/** + * Represents the type label and grouping for a transaction + */ +interface ActivityTypeLabel { + /** Base group for filtering and icon mapping */ + baseGroup: 'swaps' | 'sent' | 'received' | 'deposits' | null + /** Optional override translation key for custom labels (e.g., "Wrapped"/"Unwrapped") */ + overrideLabelKey?: string +} + +/** + * Complete row data fragments for a single transaction in the activity table + */ +export interface ActivityRowFragments { + /** Amount/token data for the transaction */ + amount?: ActivityAmountModel | null + /** Counterparty address (sender/recipient/spender) */ + counterparty?: Address | null + /** Type label and grouping information */ + typeLabel?: ActivityTypeLabel | null +} diff --git a/apps/web/src/components/ActivityTable/registry.ts b/apps/web/src/components/ActivityTable/registry.ts new file mode 100644 index 00000000000..889b6d0451a --- /dev/null +++ b/apps/web/src/components/ActivityTable/registry.ts @@ -0,0 +1,246 @@ +import { UNI_ADDRESSES } from '@uniswap/sdk-core' +import { ActivityRowFragments } from 'components/ActivityTable/activityTableModels' +import { AssetType } from 'uniswap/src/entities/assets' +import { TransactionDetails, TransactionType } from 'uniswap/src/features/transactions/types/transactionDetails' +import { getValidAddress } from 'uniswap/src/utils/addresses' +import { buildCurrencyId } from 'uniswap/src/utils/currencyId' + +/** + * Builds activity row fragments for a transaction by mapping from parsed typeInfo. + * Returns empty object for unsupported transaction types. + * + * @param details - The transaction details with parsed typeInfo + * @returns Activity row fragments containing amount, counterparty, and type label data + */ +export function buildActivityRowFragments(details: TransactionDetails): ActivityRowFragments { + const { typeInfo, chainId } = details + + switch (typeInfo.type) { + case TransactionType.Swap: + return { + amount: { + kind: 'pair', + inputCurrencyId: typeInfo.inputCurrencyId, + outputCurrencyId: typeInfo.outputCurrencyId, + inputAmountRaw: 'inputCurrencyAmountRaw' in typeInfo ? typeInfo.inputCurrencyAmountRaw : undefined, + outputAmountRaw: 'outputCurrencyAmountRaw' in typeInfo ? typeInfo.outputCurrencyAmountRaw : undefined, + }, + counterparty: null, + typeLabel: { + baseGroup: 'swaps', + overrideLabelKey: 'transaction.status.swap.success', + }, + } + + case TransactionType.Bridge: + return { + amount: { + kind: 'pair', + inputCurrencyId: typeInfo.inputCurrencyId, + outputCurrencyId: typeInfo.outputCurrencyId, + inputAmountRaw: 'inputCurrencyAmountRaw' in typeInfo ? typeInfo.inputCurrencyAmountRaw : undefined, + outputAmountRaw: 'outputCurrencyAmountRaw' in typeInfo ? typeInfo.outputCurrencyAmountRaw : undefined, + }, + counterparty: null, + typeLabel: { + baseGroup: 'swaps', + }, + } + + case TransactionType.Send: { + const currencyId = + typeInfo.assetType === AssetType.Currency ? buildCurrencyId(chainId, typeInfo.tokenAddress) : undefined + + return { + amount: { + kind: 'single', + currencyId, + amountRaw: typeInfo.currencyAmountRaw, + }, + counterparty: typeInfo.recipient ? getValidAddress({ address: typeInfo.recipient, chainId }) : null, + typeLabel: { + baseGroup: 'sent', + }, + } + } + + case TransactionType.Receive: { + const currencyId = + typeInfo.assetType === AssetType.Currency ? buildCurrencyId(chainId, typeInfo.tokenAddress) : undefined + + return { + amount: { + kind: 'single', + currencyId, + amountRaw: typeInfo.currencyAmountRaw, + }, + counterparty: typeInfo.sender ? getValidAddress({ address: typeInfo.sender, chainId }) : null, + typeLabel: { + baseGroup: 'received', + }, + } + } + + case TransactionType.Approve: { + const currencyId = buildCurrencyId(chainId, typeInfo.tokenAddress) + + return { + amount: { + kind: 'approve', + currencyId, + approvalAmount: typeInfo.approvalAmount, + }, + counterparty: typeInfo.spender ? getValidAddress({ address: typeInfo.spender, chainId }) : null, + typeLabel: { + baseGroup: null, + overrideLabelKey: 'common.approved', + }, + } + } + + case TransactionType.Wrap: + return { + amount: { + kind: 'wrap', + unwrapped: typeInfo.unwrapped, + amountRaw: typeInfo.currencyAmountRaw, + }, + counterparty: null, + typeLabel: { + baseGroup: 'swaps', + overrideLabelKey: typeInfo.unwrapped ? 'common.unwrapped' : 'common.wrapped', + }, + } + + case TransactionType.CreatePool: + case TransactionType.CreatePair: + return { + amount: { + kind: 'liquidity-pair', + currency0Id: typeInfo.currency0Id, + currency1Id: typeInfo.currency1Id, + currency0AmountRaw: typeInfo.currency0AmountRaw, + currency1AmountRaw: typeInfo.currency1AmountRaw, + }, + counterparty: typeInfo.dappInfo?.address + ? getValidAddress({ address: typeInfo.dappInfo.address, chainId }) + : null, + typeLabel: { + baseGroup: null, + overrideLabelKey: 'pool.create', + }, + } + + case TransactionType.LiquidityIncrease: + return { + amount: { + kind: 'liquidity-pair', + currency0Id: typeInfo.currency0Id, + currency1Id: typeInfo.currency1Id, + currency0AmountRaw: typeInfo.currency0AmountRaw, + currency1AmountRaw: typeInfo.currency1AmountRaw, + }, + counterparty: typeInfo.dappInfo?.address + ? getValidAddress({ address: typeInfo.dappInfo.address, chainId }) + : null, + typeLabel: { + baseGroup: 'deposits', + overrideLabelKey: 'common.addLiquidity', + }, + } + + case TransactionType.LiquidityDecrease: + return { + amount: { + kind: 'liquidity-pair', + currency0Id: typeInfo.currency0Id, + currency1Id: typeInfo.currency1Id, + currency0AmountRaw: typeInfo.currency0AmountRaw, + currency1AmountRaw: typeInfo.currency1AmountRaw, + }, + counterparty: typeInfo.dappInfo?.address + ? getValidAddress({ address: typeInfo.dappInfo.address, chainId }) + : null, + typeLabel: { + baseGroup: null, + overrideLabelKey: 'pool.removeLiquidity', + }, + } + + case TransactionType.NFTMint: { + const currencyId = typeInfo.purchaseCurrencyId + return { + amount: { + kind: 'single', + currencyId, + amountRaw: typeInfo.purchaseCurrencyAmountRaw, + }, + counterparty: typeInfo.dappInfo?.address + ? getValidAddress({ address: typeInfo.dappInfo.address, chainId }) + : null, + typeLabel: { + baseGroup: null, + overrideLabelKey: 'transaction.status.mint.success', + }, + } + } + + case TransactionType.CollectFees: + return { + amount: typeInfo.currency1Id + ? { + kind: 'liquidity-pair', + currency0Id: typeInfo.currency0Id, + currency1Id: typeInfo.currency1Id, + currency0AmountRaw: typeInfo.currency0AmountRaw, + currency1AmountRaw: typeInfo.currency1AmountRaw, + } + : { + kind: 'single', + currencyId: typeInfo.currency0Id, + amountRaw: typeInfo.currency0AmountRaw, + }, + counterparty: null, + typeLabel: { + baseGroup: null, + overrideLabelKey: 'transaction.status.collected.fees', + }, + } + + case TransactionType.LPIncentivesClaimRewards: { + const currencyId = buildCurrencyId(chainId, typeInfo.tokenAddress) + return { + amount: { + kind: 'single', + currencyId, + amountRaw: undefined, + }, + counterparty: null, + typeLabel: { + baseGroup: null, + overrideLabelKey: 'transaction.status.collected.fees', + }, + } + } + + case TransactionType.ClaimUni: { + const tokenAddress = UNI_ADDRESSES[chainId] + const currencyId = tokenAddress ? buildCurrencyId(chainId, tokenAddress) : undefined + return { + amount: { + kind: 'single', + currencyId, + amountRaw: typeInfo.uniAmountRaw, + }, + counterparty: getValidAddress({ address: typeInfo.recipient, chainId }), + typeLabel: { + baseGroup: null, + overrideLabelKey: 'common.claimed', + }, + } + } + + default: + return {} + } +} diff --git a/apps/web/src/components/Banner/BridgingPopularTokens/BridgingPopularTokensBanner.tsx b/apps/web/src/components/Banner/BridgingPopularTokens/BridgingPopularTokensBanner.tsx new file mode 100644 index 00000000000..2957dd9ef47 --- /dev/null +++ b/apps/web/src/components/Banner/BridgingPopularTokens/BridgingPopularTokensBanner.tsx @@ -0,0 +1,108 @@ +import { SharedEventName } from '@uniswap/analytics-events' +import { useCallback } from 'react' +import { useTranslation } from 'react-i18next' +import { useNavigate } from 'react-router' +import { useAppDispatch } from 'state/hooks' +import { Flex, IconButton, Image, styled, Text, TouchableArea } from 'ui/src' +import { BRIDGED_ASSETS_V2_WEB_BANNER } from 'ui/src/assets' +import { X } from 'ui/src/components/icons/X' +import { useUniswapContext } from 'uniswap/src/contexts/UniswapContext' +import { setHasDismissedBridgedAssetsBannerV2 } from 'uniswap/src/features/behaviorHistory/slice' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { ElementName } from 'uniswap/src/features/telemetry/constants' +import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' +import { Trace } from 'uniswap/src/features/telemetry/Trace' + +const BRIDGING_POPULAR_TOKENS_BANNER_HEIGHT = 152 +const GRADIENT_BACKGROUND_HEIGHT = 64 +const BANNER_PADDING = 16 + +const BannerContainer = styled(TouchableArea, { + borderRadius: '$rounded16', + width: 260, + height: BRIDGING_POPULAR_TOKENS_BANNER_HEIGHT, + shadowColor: '$shadowColor', + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.4, + shadowRadius: 10, + overflow: 'hidden', + padding: BANNER_PADDING, + backgroundColor: '$surface1', + borderWidth: 1, + borderColor: '$surface3', + gap: '$spacing16', + + '$platform-web': { + position: 'fixed', + bottom: 29, + left: 40, + }, +}) + +export function BridgingPopularTokensBanner() { + const dispatch = useAppDispatch() + const { t } = useTranslation() + const navigate = useNavigate() + const { setIsSwapTokenSelectorOpen, setSwapOutputChainId } = useUniswapContext() + + const handleBannerClose = useCallback(() => { + dispatch(setHasDismissedBridgedAssetsBannerV2(true)) + sendAnalyticsEvent(SharedEventName.ELEMENT_CLICKED, { + element: ElementName.CloseButton, + modal: ElementName.BridgedAssetsBannerV2, + }) + }, [dispatch]) + + const handleBannerClick = useCallback(() => { + navigate('/swap?outputChain=unichain') + setSwapOutputChainId(UniverseChainId.Unichain) + setIsSwapTokenSelectorOpen(true) + dispatch(setHasDismissedBridgedAssetsBannerV2(true)) + }, [dispatch, navigate, setIsSwapTokenSelectorOpen, setSwapOutputChainId]) + + return ( + + + + + + + + + {t('onboarding.home.intro.bridgedAssets.title')} + + + {t('bridgingPopularTokens.banner.description')} + + + + + ) +} + +function BannerXButton({ handleClose }: { handleClose: () => void }) { + return ( + + { + e.stopPropagation() + handleClose() + }} + hoverStyle={{ opacity: 0.8 }} + icon={} + p={2} + /> + + ) +} diff --git a/apps/web/src/components/Banner/shared/Banners.tsx b/apps/web/src/components/Banner/shared/Banners.tsx index 9ccc4ece9a5..5e16d747a68 100644 --- a/apps/web/src/components/Banner/shared/Banners.tsx +++ b/apps/web/src/components/Banner/shared/Banners.tsx @@ -1,12 +1,13 @@ import { manualChainOutageAtom, useChainOutageConfig } from 'featureFlags/flags/outageBanner' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' +import { BridgingPopularTokensBanner } from 'components/Banner/BridgingPopularTokens/BridgingPopularTokensBanner' import { getOutageBannerSessionStorageKey, OutageBanner } from 'components/Banner/Outage/OutageBanner' import { SOLANA_PROMO_BANNER_STORAGE_KEY, SolanaPromoBanner } from 'components/Banner/SolanaPromo/SolanaPromoBanner' import { useAtomValue } from 'jotai/utils' import { useMemo } from 'react' import { useLocation } from 'react-router' +import { useAppSelector } from 'state/hooks' import { UniverseChainId } from 'uniswap/src/features/chains/types' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { InterfacePageName } from 'uniswap/src/features/telemetry/constants' import { getChainIdFromChainUrlParam, isChainUrlParam } from 'utils/chainParams' import { getCurrentPageFromLocation } from 'utils/urlRoutes' @@ -15,6 +16,10 @@ export function Banners() { const { pathname } = useLocation() const currentPage = getCurrentPageFromLocation(pathname) const isSolanaPromoEnabled = useFeatureFlag(FeatureFlags.SolanaPromo) + const isBridgedAssetsBannerV2Enabled = useFeatureFlag(FeatureFlags.BridgedAssetsBannerV2) + const hasDismissedBridgedAssetsBannerV2 = useAppSelector( + (state) => state.uniswapBehaviorHistory.hasDismissedBridgedAssetsBannerV2, + ) // Read from both sources: error-detected (from GraphQL failures) and Statsig (manual config) const statsigOutage = useChainOutageConfig() @@ -55,5 +60,9 @@ export function Banners() { return } + if (isBridgedAssetsBannerV2Enabled && !hasDismissedBridgedAssetsBannerV2) { + return + } + return null } diff --git a/apps/web/src/components/Charts/D3LiquidityRangeInput/D3LiquidityRangeChart/D3LiquidityRangeChart.tsx b/apps/web/src/components/Charts/D3LiquidityRangeInput/D3LiquidityRangeChart/D3LiquidityRangeChart.tsx index 685f409f924..33afc62d804 100644 --- a/apps/web/src/components/Charts/D3LiquidityRangeInput/D3LiquidityRangeChart/D3LiquidityRangeChart.tsx +++ b/apps/web/src/components/Charts/D3LiquidityRangeInput/D3LiquidityRangeChart/D3LiquidityRangeChart.tsx @@ -12,6 +12,7 @@ import { ChartEntry } from 'components/Charts/LiquidityRangeInput/types' import { PriceChartData } from 'components/Charts/PriceChart' import { ChartType } from 'components/Charts/utils' import { useLiquidityUrlState } from 'components/Liquidity/Create/hooks/useLiquidityUrlState' +import { InitialPosition } from 'components/Liquidity/Create/types' import { ChartQueryResult } from 'components/Tokens/TokenDetails/ChartSection/util' import * as d3 from 'd3' import { useEffect, useMemo, useRef } from 'react' @@ -22,11 +23,13 @@ const D3LiquidityRangeChart = ({ liquidityData, quoteCurrency, baseCurrency, + initialPosition, }: { priceData: ChartQueryResult liquidityData: ChartEntry[] quoteCurrency: Currency baseCurrency: Currency + initialPosition?: InitialPosition }) => { const colors = useSporeColors() const svgRef = useRef(null) @@ -125,6 +128,11 @@ const D3LiquidityRangeChart = ({ useEffect(() => { let minPrice let maxPrice + + if (initialPosition) { + return + } + if (priceRangeState.minPrice && !isNaN(parseFloat(priceRangeState.minPrice))) { minPrice = parseFloat(priceRangeState.minPrice) } @@ -136,7 +144,7 @@ const D3LiquidityRangeChart = ({ minPrice, maxPrice, }) - }, [priceData.dataHash, reset]) + }, [priceData.dataHash, initialPosition, reset]) return ( diff --git a/apps/web/src/components/Charts/D3LiquidityRangeInput/D3LiquidityRangeChart/components/D3LiquidityMinMaxInput.tsx b/apps/web/src/components/Charts/D3LiquidityRangeInput/D3LiquidityRangeChart/components/D3LiquidityMinMaxInput.tsx index ee3695042fb..1e0723c11af 100644 --- a/apps/web/src/components/Charts/D3LiquidityRangeInput/D3LiquidityRangeChart/components/D3LiquidityMinMaxInput.tsx +++ b/apps/web/src/components/Charts/D3LiquidityRangeInput/D3LiquidityRangeChart/components/D3LiquidityMinMaxInput.tsx @@ -92,15 +92,26 @@ export function D3LiquidityMinMaxInput() { } const price = input === RangeSelectionInput.MIN ? minPrice : maxPrice - if (input === RangeSelectionInput.MIN && ticksAtLimit[0]) { + + if (input === RangeSelectionInput.MIN && ticksAtLimit[0] && !positionState.initialPosition) { return '0' } - if (input === RangeSelectionInput.MAX && ticksAtLimit[1]) { + if (input === RangeSelectionInput.MAX && ticksAtLimit[1] && !positionState.initialPosition) { return '∞' } + return price?.toString() ?? '' }, - [displayUserTypedValue, typedValue, inputMode, priceDifferences, minPrice, maxPrice, ticksAtLimit], + [ + displayUserTypedValue, + typedValue, + inputMode, + priceDifferences, + minPrice, + maxPrice, + ticksAtLimit, + positionState.initialPosition, + ], ) // Sets chart state but does not update liquidity context diff --git a/apps/web/src/components/Charts/D3LiquidityRangeInput/D3LiquidityRangeChart/store/types.ts b/apps/web/src/components/Charts/D3LiquidityRangeInput/D3LiquidityRangeChart/store/types.ts index 4efa9bf8fd5..d6eec00d3de 100644 --- a/apps/web/src/components/Charts/D3LiquidityRangeInput/D3LiquidityRangeChart/store/types.ts +++ b/apps/web/src/components/Charts/D3LiquidityRangeInput/D3LiquidityRangeChart/store/types.ts @@ -1,4 +1,4 @@ -import { ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' +import { ProtocolVersion } from '@uniswap/client-data-api/dist/data/v1/poolTypes_pb' import { Currency } from '@uniswap/sdk-core' import { GraphQLApi } from '@universe/api' import { TickAlignment } from 'components/Charts/D3LiquidityRangeInput/D3LiquidityRangeChart/utils/priceToY' diff --git a/apps/web/src/components/Charts/D3LiquidityRangeInput/D3LiquidityRangeChart/utils/tickUtils.ts b/apps/web/src/components/Charts/D3LiquidityRangeInput/D3LiquidityRangeChart/utils/tickUtils.ts index a3385a91640..5b7eb089cf3 100644 --- a/apps/web/src/components/Charts/D3LiquidityRangeInput/D3LiquidityRangeChart/utils/tickUtils.ts +++ b/apps/web/src/components/Charts/D3LiquidityRangeInput/D3LiquidityRangeChart/utils/tickUtils.ts @@ -1,4 +1,4 @@ -import { ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' +import { ProtocolVersion } from '@uniswap/client-data-api/dist/data/v1/poolTypes_pb' import { nearestUsableTick, priceToClosestTick, TickMath, tickToPrice as tickToPriceV3 } from '@uniswap/v3-sdk' import { priceToClosestTick as priceToClosestV4Tick, tickToPrice as tickToPriceV4 } from '@uniswap/v4-sdk' import { TickNavigationParams } from 'components/Charts/D3LiquidityRangeInput/D3LiquidityRangeChart/store/types' diff --git a/apps/web/src/components/Charts/D3LiquidityRangeInput/D3LiquidityRangeInput.tsx b/apps/web/src/components/Charts/D3LiquidityRangeInput/D3LiquidityRangeInput.tsx index 205c72ed202..71128df63fe 100644 --- a/apps/web/src/components/Charts/D3LiquidityRangeInput/D3LiquidityRangeInput.tsx +++ b/apps/web/src/components/Charts/D3LiquidityRangeInput/D3LiquidityRangeInput.tsx @@ -1,4 +1,4 @@ -import { ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' +import { ProtocolVersion } from '@uniswap/client-data-api/dist/data/v1/poolTypes_pb' import { Currency, Price } from '@uniswap/sdk-core' import { GraphQLApi } from '@universe/api' import { D3LiquidityChartHeader } from 'components/Charts/D3LiquidityRangeInput/D3LiquidityRangeChart/components/D3LiquidityChartHeader' @@ -13,7 +13,7 @@ import { ChartEntry } from 'components/Charts/LiquidityRangeInput/types' import { ChartSkeleton } from 'components/Charts/LoadingState' import { PriceChartData } from 'components/Charts/PriceChart' import { ChartType } from 'components/Charts/utils' -import { RangeAmountInputPriceMode } from 'components/Liquidity/Create/types' +import { InitialPosition, RangeAmountInputPriceMode } from 'components/Liquidity/Create/types' import { usePoolPriceChartData } from 'hooks/usePoolPriceChartData' import { UTCTimestamp } from 'lightweight-charts' import { useMemo, useState } from 'react' @@ -42,6 +42,7 @@ export function D3LiquidityRangeInput({ price, hook, currentPrice, + initialPosition, isFullRange, minPrice, maxPrice, @@ -73,6 +74,7 @@ export function D3LiquidityRangeInput({ minPrice?: number maxPrice?: number inputMode?: RangeAmountInputPriceMode + initialPosition?: InitialPosition setInputMode: (inputMode: RangeAmountInputPriceMode) => void setMinPrice: (minPrice?: number | null) => void setMaxPrice: (maxPrice?: number | null) => void @@ -194,6 +196,7 @@ export function D3LiquidityRangeInput({ baseCurrency={baseCurrency} priceData={finalPriceData} liquidityData={sortedLiquidityData} + initialPosition={initialPosition} /> ) : ( diff --git a/apps/web/src/components/Charts/LiquidityChart/index.tsx b/apps/web/src/components/Charts/LiquidityChart/index.tsx index cf4dfbf7f2a..b5aff745efb 100644 --- a/apps/web/src/components/Charts/LiquidityChart/index.tsx +++ b/apps/web/src/components/Charts/LiquidityChart/index.tsx @@ -1,5 +1,5 @@ import { BigNumber } from '@ethersproject/bignumber' -import { ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' +import { ProtocolVersion } from '@uniswap/client-data-api/dist/data/v1/poolTypes_pb' import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core' import { FeeAmount, Pool as PoolV3, TICK_SPACINGS, TickMath as TickMathV3, tickToPrice } from '@uniswap/v3-sdk' import { Pool as PoolV4, tickToPrice as tickToPriceV4 } from '@uniswap/v4-sdk' diff --git a/apps/web/src/components/Charts/LiquidityPositionRangeChart/LiquidityPositionRangeChart.tsx b/apps/web/src/components/Charts/LiquidityPositionRangeChart/LiquidityPositionRangeChart.tsx index f22dce5006c..e820c97f067 100644 --- a/apps/web/src/components/Charts/LiquidityPositionRangeChart/LiquidityPositionRangeChart.tsx +++ b/apps/web/src/components/Charts/LiquidityPositionRangeChart/LiquidityPositionRangeChart.tsx @@ -1,4 +1,4 @@ -import { PositionStatus, ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' +import { PositionStatus, ProtocolVersion } from '@uniswap/client-data-api/dist/data/v1/poolTypes_pb' import { Currency, Price } from '@uniswap/sdk-core' import { Pair } from '@uniswap/v2-sdk' import { FeeAmount, Pool as V3Pool } from '@uniswap/v3-sdk' diff --git a/apps/web/src/components/Charts/LiquidityRangeInput/LiquidityRangeInput.tsx b/apps/web/src/components/Charts/LiquidityRangeInput/LiquidityRangeInput.tsx index af38a075534..6ad03dc900c 100644 --- a/apps/web/src/components/Charts/LiquidityRangeInput/LiquidityRangeInput.tsx +++ b/apps/web/src/components/Charts/LiquidityRangeInput/LiquidityRangeInput.tsx @@ -1,4 +1,4 @@ -import { ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' +import { ProtocolVersion } from '@uniswap/client-data-api/dist/data/v1/poolTypes_pb' import { Currency } from '@uniswap/sdk-core' import { GraphQLApi } from '@universe/api' import { ActiveLiquidityChart } from 'components/Charts/ActiveLiquidityChart/ActiveLiquidityChart' diff --git a/apps/web/src/components/Charts/LiquidityRangeInput/hooks.ts b/apps/web/src/components/Charts/LiquidityRangeInput/hooks.ts index 907bb46ba51..22f595d3efc 100644 --- a/apps/web/src/components/Charts/LiquidityRangeInput/hooks.ts +++ b/apps/web/src/components/Charts/LiquidityRangeInput/hooks.ts @@ -1,5 +1,5 @@ import { useQuery } from '@tanstack/react-query' -import { ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' +import { ProtocolVersion } from '@uniswap/client-data-api/dist/data/v1/poolTypes_pb' import { Currency } from '@uniswap/sdk-core' import { calculateTokensLockedV3, calculateTokensLockedV4 } from 'components/Charts/LiquidityChart' import { ChartEntry } from 'components/Charts/LiquidityRangeInput/types' diff --git a/apps/web/src/components/Charts/PriceChart/index.tsx b/apps/web/src/components/Charts/PriceChart/index.tsx index f3767252cd6..2313ca3d480 100644 --- a/apps/web/src/components/Charts/PriceChart/index.tsx +++ b/apps/web/src/components/Charts/PriceChart/index.tsx @@ -25,6 +25,7 @@ import { Trans } from 'react-i18next' import { Flex, styled, Text } from 'ui/src' import { opacify } from 'ui/src/theme' import { isLowVarianceRange } from 'uniswap/src/components/charts/utils' +import { useFormatChartFiatDelta } from 'uniswap/src/features/fiatCurrency/hooks/useFormatChartFiatDelta' import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext' import { NumberType } from 'utilities/src/format/types' @@ -230,19 +231,54 @@ export class PriceChartModel extends ChartModel { } interface PriceChartDeltaProps { - startingPrice: PriceChartData - endingPrice: PriceChartData + startingPrice: number + endingPrice: number noColor?: boolean + shouldIncludeFiatDelta?: boolean + shouldTreatAsStablecoin?: boolean } -export function PriceChartDelta({ startingPrice, endingPrice, noColor }: PriceChartDeltaProps) { - const delta = calculateDelta(startingPrice.close, endingPrice.close) - const { formatPercent } = useLocalizationContext() +export function PriceChartDelta({ + startingPrice, + endingPrice, + noColor, + shouldIncludeFiatDelta = false, + shouldTreatAsStablecoin = false, +}: PriceChartDeltaProps) { + const { formatPercent, convertFiatAmount } = useLocalizationContext() + const { formatChartFiatDelta } = useFormatChartFiatDelta() + + const delta = calculateDelta(startingPrice, endingPrice) + const formattedDelta = useMemo(() => { + return delta !== undefined ? formatPercent(Math.abs(delta)) : '-' + }, [delta, formatPercent]) + + const fiatDelta = useMemo(() => { + if (!shouldIncludeFiatDelta) { + return null + } + + const convertedStart = convertFiatAmount(startingPrice) + const convertedEnd = convertFiatAmount(endingPrice) + + return formatChartFiatDelta({ + startingPrice: convertedStart.amount, + endingPrice: convertedEnd.amount, + isStablecoin: shouldTreatAsStablecoin, + }) + }, [ + shouldIncludeFiatDelta, + formatChartFiatDelta, + startingPrice, + endingPrice, + convertFiatAmount, + shouldTreatAsStablecoin, + ]) return ( - {delta && } - {delta ? formatPercent(Math.abs(delta)) : '-'} + {delta !== undefined && } + {fiatDelta ? `${fiatDelta.formatted} (${formattedDelta})` : formattedDelta} ) } @@ -288,7 +324,14 @@ function CandlestickTooltip({ data }: { data: PriceChartData }) { } export function PriceChart({ data, height, type, stale, timePeriod }: PriceChartProps) { + const startingPrice = data[0] const lastPrice = data[data.length - 1] + const { min, max } = getCandlestickPriceBounds(data) + const shouldTreatAsStablecoin = isLowVarianceRange({ + min, + max, + duration: timePeriod, + }) return ( ( } + additionalFields={ + + } valueFormatterType={NumberType.FiatTokenPrice} time={crosshairData?.time} /> diff --git a/apps/web/src/components/Expand/index.tsx b/apps/web/src/components/Expand/index.tsx index aa3303a3209..d9831b552f8 100644 --- a/apps/web/src/components/Expand/index.tsx +++ b/apps/web/src/components/Expand/index.tsx @@ -1,31 +1,8 @@ -import Column from 'components/deprecated/Column' -import Row, { RowBetween } from 'components/deprecated/Row' -import styled from 'lib/styled-components' import { PropsWithChildren, ReactElement } from 'react' -import { ChevronDown } from 'react-feather' -import { HeightAnimator } from 'ui/src' +import { Flex, FlexProps, HeightAnimator } from 'ui/src' +import { RotatableChevron } from 'ui/src/components/icons/RotatableChevron' import { iconSizes } from 'ui/src/theme' -const ButtonContainer = styled(Row)` - cursor: pointer; - justify-content: flex-end; - width: unset; -` - -const ExpandIcon = styled(ChevronDown)<{ $isOpen: boolean }>` - color: ${({ theme }) => theme.neutral2}; - transform: ${({ $isOpen }) => ($isOpen ? 'rotate(180deg)' : 'rotate(0deg)')}; - transition: transform ${({ theme }) => theme.transition.duration.medium}; -` - -const Content = styled(Column)` - padding-top: ${({ theme }) => theme.grids.md}; -` - -const Wrapper = styled(Column)<{ $padding?: string }>` - padding: ${({ $padding }) => $padding}; -` - export default function Expand({ header, button, @@ -35,27 +12,41 @@ export default function Expand({ padding, onToggle, iconSize = 'icon24', + paddingTop, + width, }: PropsWithChildren<{ header?: ReactElement button: ReactElement testId?: string isOpen: boolean - padding?: string + padding?: FlexProps['p'] onToggle: () => void iconSize?: keyof typeof iconSizes + paddingTop?: FlexProps['pt'] + width?: FlexProps['width'] }>) { return ( - - + + {header} - + {button} - - - + + + - {children} + + {children} + - + ) } diff --git a/apps/web/src/components/FeatureFlagModal/FeatureFlagModal.tsx b/apps/web/src/components/FeatureFlagModal/FeatureFlagModal.tsx index d419cc0f67d..0b3d9913486 100644 --- a/apps/web/src/components/FeatureFlagModal/FeatureFlagModal.tsx +++ b/apps/web/src/components/FeatureFlagModal/FeatureFlagModal.tsx @@ -1,3 +1,15 @@ +import type { DynamicConfigKeys } from '@universe/gating' +import { + DynamicConfigs, + Experiments, + ExternallyConnectableExtensionConfigKey, + FeatureFlags, + getFeatureFlagName, + getOverrideAdapter, + Layers, + NetworkRequestsConfigKey, + useFeatureFlagWithExposureLoggingDisabled, +} from '@universe/gating' import { useModalState } from 'hooks/useModalState' import styledDep from 'lib/styled-components' import { useExternallyConnectableExtensionId } from 'pages/ExtensionPasskeyAuthPopUp/useExternallyConnectableExtensionId' @@ -6,16 +18,6 @@ import { useCallback } from 'react' import { Button, Flex, ModalCloseIcon, styled, Text } from 'ui/src' import { ExperimentRow, LayerRow } from 'uniswap/src/components/gating/Rows' import { Modal } from 'uniswap/src/components/modals/Modal' -import type { DynamicConfigKeys } from 'uniswap/src/features/gating/configs' -import { - DynamicConfigs, - ExternallyConnectableExtensionConfigKey, - NetworkRequestsConfigKey, -} from 'uniswap/src/features/gating/configs' -import { Experiments, Layers } from 'uniswap/src/features/gating/experiments' -import { FeatureFlags, getFeatureFlagName } from 'uniswap/src/features/gating/flags' -import { useFeatureFlagWithExposureLoggingDisabled } from 'uniswap/src/features/gating/hooks' -import { getOverrideAdapter } from 'uniswap/src/features/gating/sdk/statsig' import { ModalName } from 'uniswap/src/features/telemetry/constants' import { isPlaywrightEnv } from 'utilities/src/environment/env' import { TRUSTED_CHROME_EXTENSION_IDS } from 'utilities/src/environment/extensionId' @@ -276,6 +278,7 @@ export default function FeatureFlagModal() { + @@ -286,6 +289,7 @@ export default function FeatureFlagModal() { + diff --git a/apps/web/src/components/HelpModal/HelpContent.tsx b/apps/web/src/components/HelpModal/HelpContent.tsx index b67856643ae..4e3332c04b6 100644 --- a/apps/web/src/components/HelpModal/HelpContent.tsx +++ b/apps/web/src/components/HelpModal/HelpContent.tsx @@ -32,7 +32,8 @@ export function HelpContent({ onClose }: HelpContentProps) { return ( - setIsOpen(open)}> + setIsOpen(open)} + > - + + + setIsOpen(false)} /> diff --git a/apps/web/src/components/Liquidity/ClaimFeeModal.tsx b/apps/web/src/components/Liquidity/ClaimFeeModal.tsx index 330fceecad5..bd924749dad 100644 --- a/apps/web/src/components/Liquidity/ClaimFeeModal.tsx +++ b/apps/web/src/components/Liquidity/ClaimFeeModal.tsx @@ -1,4 +1,4 @@ -import { ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' +import { ProtocolVersion } from '@uniswap/client-data-api/dist/data/v1/poolTypes_pb' import { CurrencyAmount } from '@uniswap/sdk-core' import { TradingApi } from '@universe/api' import { ErrorCallout } from 'components/ErrorCallout' diff --git a/apps/web/src/components/Liquidity/Create/AddHook.tsx b/apps/web/src/components/Liquidity/Create/AddHook.tsx index 2620a7cf2e9..506d1aa9dc6 100644 --- a/apps/web/src/components/Liquidity/Create/AddHook.tsx +++ b/apps/web/src/components/Liquidity/Create/AddHook.tsx @@ -1,4 +1,4 @@ -import { ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' +import { ProtocolVersion } from '@uniswap/client-data-api/dist/data/v1/poolTypes_pb' import { AdvancedButton } from 'components/Liquidity/Create/AdvancedButton' import { useLiquidityUrlState } from 'components/Liquidity/Create/hooks/useLiquidityUrlState' import { DEFAULT_POSITION_STATE } from 'components/Liquidity/Create/types' diff --git a/apps/web/src/components/Liquidity/Create/EditStep.tsx b/apps/web/src/components/Liquidity/Create/EditStep.tsx index bd96f6af8bc..ba5d25fd7c7 100644 --- a/apps/web/src/components/Liquidity/Create/EditStep.tsx +++ b/apps/web/src/components/Liquidity/Create/EditStep.tsx @@ -1,4 +1,4 @@ -import { ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' +import { ProtocolVersion } from '@uniswap/client-data-api/dist/data/v1/poolTypes_pb' import CreatingPoolInfo from 'components/CreatingPoolInfo/CreatingPoolInfo' import { useDefaultInitialPrice } from 'components/Liquidity/Create/hooks/useDefaultInitialPrice' import { PositionFlowStep } from 'components/Liquidity/Create/types' diff --git a/apps/web/src/components/Liquidity/Create/FormWrapper.tsx b/apps/web/src/components/Liquidity/Create/FormWrapper.tsx index 8a172f68a51..07713ff025b 100644 --- a/apps/web/src/components/Liquidity/Create/FormWrapper.tsx +++ b/apps/web/src/components/Liquidity/Create/FormWrapper.tsx @@ -1,4 +1,4 @@ -import { ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' +import { ProtocolVersion } from '@uniswap/client-data-api/dist/data/v1/poolTypes_pb' import { Currency } from '@uniswap/sdk-core' import { BreadcrumbNavContainer, BreadcrumbNavLink } from 'components/BreadcrumbNav' import { Container } from 'components/Liquidity/Create/Container' diff --git a/apps/web/src/components/Liquidity/Create/PositionOutOfRangeError.tsx b/apps/web/src/components/Liquidity/Create/PositionOutOfRangeError.tsx index b5e243de478..76aab9ca1cc 100644 --- a/apps/web/src/components/Liquidity/Create/PositionOutOfRangeError.tsx +++ b/apps/web/src/components/Liquidity/Create/PositionOutOfRangeError.tsx @@ -1,4 +1,4 @@ -import { PositionStatus } from '@uniswap/client-pools/dist/pools/v1/types_pb' +import { PositionStatus } from '@uniswap/client-data-api/dist/data/v1/poolTypes_pb' import { ErrorCallout } from 'components/ErrorCallout' import { PositionInfo } from 'components/Liquidity/types' import { useTranslation } from 'react-i18next' diff --git a/apps/web/src/components/Liquidity/Create/RangeSelectionStep.tsx b/apps/web/src/components/Liquidity/Create/RangeSelectionStep.tsx index 80c51c9c44e..7ad8a3b54b9 100644 --- a/apps/web/src/components/Liquidity/Create/RangeSelectionStep.tsx +++ b/apps/web/src/components/Liquidity/Create/RangeSelectionStep.tsx @@ -1,4 +1,5 @@ -import { ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' +import { ProtocolVersion } from '@uniswap/client-data-api/dist/data/v1/poolTypes_pb' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { D3LiquidityRangeInput } from 'components/Charts/D3LiquidityRangeInput/D3LiquidityRangeInput' import { LiquidityRangeInput } from 'components/Charts/LiquidityRangeInput/LiquidityRangeInput' import { useDefaultInitialPrice } from 'components/Liquidity/Create/hooks/useDefaultInitialPrice' @@ -23,8 +24,6 @@ import { AlertTriangleFilled } from 'ui/src/components/icons/AlertTriangleFilled import { fonts, zIndexes } from 'ui/src/theme' import { AmountInput } from 'uniswap/src/components/AmountInput/AmountInput' import { WarningSeverity } from 'uniswap/src/components/modals/WarningModal/types' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' enum RangeSelection { FULL = 'FULL', @@ -535,6 +534,7 @@ export const SelectPriceRangeStep = ({ price={price} currentPrice={Number(price?.toSignificant())} inputMode={priceRangeState.inputMode} + initialPosition={initialPosition} minPrice={rangeInputMinPrice} maxPrice={rangeInputMaxPrice} isFullRange={priceRangeState.fullRange} diff --git a/apps/web/src/components/Liquidity/Create/SelectTokenStep.tsx b/apps/web/src/components/Liquidity/Create/SelectTokenStep.tsx index 86470371e61..95762e764d7 100644 --- a/apps/web/src/components/Liquidity/Create/SelectTokenStep.tsx +++ b/apps/web/src/components/Liquidity/Create/SelectTokenStep.tsx @@ -1,6 +1,13 @@ import { PrefetchBalancesWrapper } from 'appGraphql/data/apollo/AdaptiveTokenBalancesProvider' -import { ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' +import { ProtocolVersion } from '@uniswap/client-data-api/dist/data/v1/poolTypes_pb' import type { Currency, Percent } from '@uniswap/sdk-core' +import { + AllowedV4WethHookAddressesConfigKey, + DynamicConfigs, + FeatureFlags, + useDynamicConfigValue, + useFeatureFlag, +} from '@universe/gating' import CreatingPoolInfo from 'components/CreatingPoolInfo/CreatingPoolInfo' import { ErrorCallout } from 'components/ErrorCallout' import { AddHook } from 'components/Liquidity/Create/AddHook' @@ -41,9 +48,6 @@ import { useUrlContext } from 'uniswap/src/contexts/UrlContext' import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { UniverseChainId } from 'uniswap/src/features/chains/types' import type { CurrencyInfo } from 'uniswap/src/features/dataApi/types' -import { AllowedV4WethHookAddressesConfigKey, DynamicConfigs } from 'uniswap/src/features/gating/configs' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useDynamicConfigValue, useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext' import { Platform } from 'uniswap/src/features/platforms/types/Platform' import { isSVMChain } from 'uniswap/src/features/platforms/utils/chains' @@ -225,7 +229,7 @@ export function SelectTokensStep({ }) const { - positionState: { hook, userApprovedHook, fee }, + positionState: { hook, userApprovedHook, fee, initialPosition }, setPositionState, protocolVersion, creatingPoolOrPair, @@ -586,7 +590,7 @@ export function SelectTokensStep({ + + + ) +} diff --git a/apps/web/src/pages/Portfolio/ConnectWalletBottomOverlay.tsx b/apps/web/src/pages/Portfolio/ConnectWalletBottomOverlay.tsx new file mode 100644 index 00000000000..1d3023714b3 --- /dev/null +++ b/apps/web/src/pages/Portfolio/ConnectWalletBottomOverlay.tsx @@ -0,0 +1,47 @@ +import { useAccountDrawer } from 'components/AccountDrawer/MiniPortfolio/hooks' +import { useTranslation } from 'react-i18next' +import { Button, Flex, Text } from 'ui/src' + +export function ConnectWalletBottomOverlay(): JSX.Element { + const accountDrawer = useAccountDrawer() + const { t } = useTranslation() + + return ( + + + + {t('portfolio.disconnected.connectWallet.cta')} + + + + + ) +} diff --git a/apps/web/src/pages/Portfolio/ConnectWalletView.tsx b/apps/web/src/pages/Portfolio/ConnectWalletView.tsx deleted file mode 100644 index 2cbbd7b7d11..00000000000 --- a/apps/web/src/pages/Portfolio/ConnectWalletView.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { useAccountDrawer } from 'components/AccountDrawer/MiniPortfolio/hooks' -import { useTranslation } from 'react-i18next' -import { Button, Flex, Text } from 'ui/src' -import { LineChartDots } from 'ui/src/components/icons/LineChartDots' -import { iconSizes } from 'ui/src/theme' -import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' - -export default function PortfolioConnectWalletView() { - const { t } = useTranslation() - const accountDrawer = useAccountDrawer() - const { chains } = useEnabledChains() - - return ( - - - - - - - {t('common.getStarted')} - - {t('portfolio.connectWallet.summary', { amount: chains.length })} - - - - - - ) -} diff --git a/apps/web/src/pages/Portfolio/Header/Header.tsx b/apps/web/src/pages/Portfolio/Header/Header.tsx index dabe6593763..c08767e2216 100644 --- a/apps/web/src/pages/Portfolio/Header/Header.tsx +++ b/apps/web/src/pages/Portfolio/Header/Header.tsx @@ -1,14 +1,11 @@ import NetworkFilter from 'components/NetworkFilter/NetworkFilter' -import { useAccount } from 'hooks/useAccount' -import { useScroll } from 'hooks/useScroll' import { usePortfolioParams } from 'pages/Portfolio/Header/hooks/usePortfolioParams' +import PortfolioAddressDisplay from 'pages/Portfolio/Header/PortfolioAddressDisplay/PortfolioAddressDisplay' import { PortfolioTabs } from 'pages/Portfolio/Header/Tabs' import { PortfolioTab } from 'pages/Portfolio/types' -import { useEffect, useState } from 'react' import { useNavigate } from 'react-router' import { Flex } from 'ui/src' import { INTERFACE_NAV_HEIGHT } from 'ui/src/theme/heights' -import { AddressDisplay } from 'uniswap/src/components/accounts/AddressDisplay' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { useEvent } from 'utilities/src/react/hooks' import { getChainUrlParam } from 'utils/chainParams' @@ -22,21 +19,6 @@ function buildPortfolioUrl(tab: PortfolioTab | undefined, chainId: UniverseChain export default function PortfolioHeader() { const navigate = useNavigate() const { tab, chainId: currentChainId } = usePortfolioParams() - const { height: scrollHeight } = useScroll() - const [isCompact, setIsCompact] = useState(false) - const account = useAccount() - - useEffect(() => { - setIsCompact((prevIsCompact) => { - if (!prevIsCompact && scrollHeight > 120) { - return true - } - if (prevIsCompact && scrollHeight < 80) { - return false - } - return prevIsCompact - }) - }, [scrollHeight]) const onNetworkPress = useEvent((chainId: UniverseChainId | undefined) => { navigate(buildPortfolioUrl(tab, chainId)) @@ -56,14 +38,8 @@ export default function PortfolioHeader() { > - + + { + setIsCompact((prevIsCompact) => { + if (!prevIsCompact && scrollHeight > 120) { + return true + } + if (prevIsCompact && scrollHeight < 80) { + return false + } + return prevIsCompact + }) + }, [scrollHeight]) + + if (!account.address) { + return null + } + + return ( + + ) +} diff --git a/apps/web/src/pages/Portfolio/Header/PortfolioAddressDisplay/DemoAddressDisplay.tsx b/apps/web/src/pages/Portfolio/Header/PortfolioAddressDisplay/DemoAddressDisplay.tsx new file mode 100644 index 00000000000..4d6166b96cf --- /dev/null +++ b/apps/web/src/pages/Portfolio/Header/PortfolioAddressDisplay/DemoAddressDisplay.tsx @@ -0,0 +1,37 @@ +import { ReactComponent as Unicon } from 'assets/svg/Emblem/default.svg' +import { useTranslation } from 'react-i18next' +import { Flex, Text, Tooltip, useSporeColors } from 'ui/src' +import { Eye } from 'ui/src/components/icons/Eye' +import { iconSizes } from 'ui/src/theme' + +export default function DemoAddressDisplay() { + const colors = useSporeColors() + const { t } = useTranslation() + + return ( + + + + + + + + + {t('portfolio.disconnected.demoWallet.title')} + + + + + + {t('portfolio.disconnected.demoWallet.description')} + + + + ) +} diff --git a/apps/web/src/pages/Portfolio/Header/PortfolioAddressDisplay/PortfolioAddressDisplay.tsx b/apps/web/src/pages/Portfolio/Header/PortfolioAddressDisplay/PortfolioAddressDisplay.tsx new file mode 100644 index 00000000000..c2f6a8b9fbf --- /dev/null +++ b/apps/web/src/pages/Portfolio/Header/PortfolioAddressDisplay/PortfolioAddressDisplay.tsx @@ -0,0 +1,9 @@ +import useIsConnected from 'pages/Portfolio/Header/hooks/useIsConnected' +import ConnectedAddressDisplay from 'pages/Portfolio/Header/PortfolioAddressDisplay/ConnectedAddressDisplay' +import DemoAddressDisplay from 'pages/Portfolio/Header/PortfolioAddressDisplay/DemoAddressDisplay' + +export default function PortfolioAddressDisplay(): JSX.Element { + const isConnected = useIsConnected() + + return isConnected ? : +} diff --git a/apps/web/src/pages/Portfolio/Header/hooks/useIsConnected.ts b/apps/web/src/pages/Portfolio/Header/hooks/useIsConnected.ts new file mode 100644 index 00000000000..6c381c705ec --- /dev/null +++ b/apps/web/src/pages/Portfolio/Header/hooks/useIsConnected.ts @@ -0,0 +1,7 @@ +/* eslint-disable-next-line no-restricted-imports, no-restricted-syntax */ +import { useAccount } from 'hooks/useAccount' + +export default function useIsConnected() { + const account = useAccount() + return !!account.address +} diff --git a/apps/web/src/pages/Portfolio/NFTs/NFTCard.tsx b/apps/web/src/pages/Portfolio/NFTs/NFTCard.tsx new file mode 100644 index 00000000000..878c0a3cab8 --- /dev/null +++ b/apps/web/src/pages/Portfolio/NFTs/NFTCard.tsx @@ -0,0 +1,144 @@ +import { SharedEventName } from '@uniswap/analytics-events' +import { useCallback, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { AnimateTransition, Flex, Text, TouchableArea, useSporeColors } from 'ui/src' +import { ArrowUpRight } from 'ui/src/components/icons/ArrowUpRight' +import { MoreHorizontal } from 'ui/src/components/icons/MoreHorizontal' +import { zIndexes } from 'ui/src/theme' +import { iconSizes } from 'ui/src/theme/iconSizes' +import { NetworkLogo } from 'uniswap/src/components/CurrencyLogo/NetworkLogo' +import { NftView, NftViewProps } from 'uniswap/src/components/nfts/NftView' +import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' +import { ElementName, SectionName } from 'uniswap/src/features/telemetry/constants' +import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' +import { getOpenseaLink, openUri } from 'uniswap/src/utils/linking' + +const FLOAT_UP_ON_HOVER_OFFSET = -4 + +/** + * Generates a unique rotation angle for an element based on its ID + * @param id - Unique identifier for the element + * @returns CSS custom property object with rotation value + */ +function generateRotationStyle(id: string) { + // Generate hash from ID + const hashCode = id.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) + + // Determine rotation direction (positive or negative) + const direction = hashCode % 2 === 0 ? 1 : -1 + + // Generate rotation amount between 0.5 and 2.5 degrees + const rotationAmount = 0.5 + (hashCode % 201) / 100 // Range: 0.5 to 2.5 + return direction * rotationAmount +} + +type NftCardProps = Omit & { + owner: Address + id: string + onPress?: () => void +} + +export function NFTCard(props: NftCardProps): JSX.Element { + const [isHovered, setIsHovered] = useState(false) + const colors = useSporeColors() + const { t } = useTranslation() + + // Generate OpenSea URL for the NFT + const openseaUrl = useMemo(() => { + if (props.item.chain && props.item.contractAddress && props.item.tokenId) { + const chainId = fromGraphQLChain(props.item.chain) + if (chainId) { + return getOpenseaLink({ + chainId, + contractAddress: props.item.contractAddress, + tokenId: props.item.tokenId, + }) + } + } + return null + }, [props.item.chain, props.item.contractAddress, props.item.tokenId]) + + const handlePress = useCallback(async () => { + if (openseaUrl) { + await openUri({ uri: openseaUrl }) + } + sendAnalyticsEvent(SharedEventName.ELEMENT_CLICKED, { + element: ElementName.PortfolioNftItem, + section: SectionName.PortfolioNftsTab, + collection_name: props.item.collectionName, + collection_address: props.item.contractAddress, + token_id: props.item.tokenId, + }) + props.onPress?.() + }, [openseaUrl, props.item.collectionName, props.item.contractAddress, props.item.tokenId, props.onPress]) + + return ( + + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + onPress={handlePress} + > + {/* Context menu trigger icon */} + {/* TODO: open NFT context menu on click */} + event.stopPropagation()} + > + + + + {/* Let the parent card handle the onPress */} + {}} /> + + + + {props.item.name} + + + + + {props.item.collectionName} + + {props.item.chain && } + + + + {t('common.opensea.link')} + + + + + + + + ) +} diff --git a/apps/web/src/pages/Portfolio/NFTs/Nfts.tsx b/apps/web/src/pages/Portfolio/NFTs/Nfts.tsx new file mode 100644 index 00000000000..d876b5a78f2 --- /dev/null +++ b/apps/web/src/pages/Portfolio/NFTs/Nfts.tsx @@ -0,0 +1,75 @@ +import { SearchInput } from 'pages/Portfolio/components/SearchInput' +import { usePortfolioAddress } from 'pages/Portfolio/hooks/usePortfolioAddress' +import { NFTCard } from 'pages/Portfolio/NFTs/NFTCard' +import { filterNft } from 'pages/Portfolio/NFTs/utils/filterNfts' +import { useCallback, useMemo, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { Flex, Text } from 'ui/src' +import { useNftListRenderData } from 'uniswap/src/components/nfts/hooks/useNftListRenderData' +import { NftsList } from 'uniswap/src/components/nfts/NftsList' +import { NFTItem } from 'uniswap/src/features/nfts/types' +import { InterfacePageName } from 'uniswap/src/features/telemetry/constants' +import Trace from 'uniswap/src/features/telemetry/Trace' +import { assume0xAddress } from 'utils/wagmi' + +const LOADING_SKELETON_COUNT = 10 + +export default function PortfolioNfts(): JSX.Element { + const { t } = useTranslation() + const owner = usePortfolioAddress() + const nftsContainerRef = useRef(null) + + const [search, setSearch] = useState('') + const lowercaseSearch = useMemo(() => search.trim().toLowerCase(), [search]) + + const { numShown } = useNftListRenderData({ owner: assume0xAddress(owner), skip: !owner }) + + const renderNFTItem = useCallback( + (item: NFTItem) => { + if (!filterNft(item, lowercaseSearch)) { + return + } + + return ( + + + + + + ) + }, + [lowercaseSearch, owner], + ) + + return ( + + + + + {numShown ? `${numShown}` : ''} {t('portfolio.nfts.title')} + + + + + + + + + + ) +} diff --git a/apps/web/src/pages/Portfolio/NFTs/utils/filterNfts.test.ts b/apps/web/src/pages/Portfolio/NFTs/utils/filterNfts.test.ts new file mode 100644 index 00000000000..b208030e60d --- /dev/null +++ b/apps/web/src/pages/Portfolio/NFTs/utils/filterNfts.test.ts @@ -0,0 +1,257 @@ +import { filterNft } from 'pages/Portfolio/NFTs/utils/filterNfts' +import { NFTItem } from 'uniswap/src/features/nfts/types' + +describe('filterNft', () => { + const createMockNft = (overrides: Partial = {}): NFTItem => ({ + name: 'Bored Ape #1234', + collectionName: 'Bored Ape Yacht Club', + tokenId: '1234', + contractAddress: '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D', + ...overrides, + }) + + describe('when search query is empty', () => { + it('should return true for empty string', () => { + const nft = createMockNft() + expect(filterNft(nft, '')).toBe(true) + }) + + it('should return true for whitespace-only string', () => { + const nft = createMockNft() + expect(filterNft(nft, ' ')).toBe(true) + }) + + it('should return true for null/undefined search', () => { + const nft = createMockNft() + expect(filterNft(nft, '')).toBe(true) + }) + }) + + describe('when searching by NFT name', () => { + it('should match exact name', () => { + const nft = createMockNft({ name: 'Bored Ape #1234' }) + expect(filterNft(nft, 'Bored Ape #1234')).toBe(true) + }) + + it('should match partial name', () => { + const nft = createMockNft({ name: 'Bored Ape #1234' }) + expect(filterNft(nft, 'Bored')).toBe(true) + }) + + it('should be case-insensitive', () => { + const nft = createMockNft({ name: 'Bored Ape #1234' }) + expect(filterNft(nft, 'bored')).toBe(true) + expect(filterNft(nft, 'BORED')).toBe(true) + expect(filterNft(nft, 'BoReD')).toBe(true) + }) + + it('should not match when name does not contain search term', () => { + const nft = createMockNft({ name: 'Bored Ape #1234' }) + expect(filterNft(nft, 'CryptoPunk')).toBe(false) + }) + + it('should handle undefined name', () => { + const nft = createMockNft({ + name: undefined, + collectionName: undefined, + tokenId: undefined, + contractAddress: undefined, + }) + expect(filterNft(nft, 'Bored')).toBe(false) + }) + + it('should handle null name', () => { + const nft = createMockNft({ + name: null as any, + collectionName: undefined, + tokenId: undefined, + contractAddress: undefined, + }) + expect(filterNft(nft, 'Bored')).toBe(false) + }) + }) + + describe('when searching by collection name', () => { + it('should match exact collection name', () => { + const nft = createMockNft({ collectionName: 'Bored Ape Yacht Club' }) + expect(filterNft(nft, 'Bored Ape Yacht Club')).toBe(true) + }) + + it('should match partial collection name', () => { + const nft = createMockNft({ collectionName: 'Bored Ape Yacht Club' }) + expect(filterNft(nft, 'Yacht')).toBe(true) + }) + + it('should be case-insensitive', () => { + const nft = createMockNft({ collectionName: 'Bored Ape Yacht Club' }) + expect(filterNft(nft, 'yacht')).toBe(true) + expect(filterNft(nft, 'YACHT')).toBe(true) + expect(filterNft(nft, 'YaChT')).toBe(true) + }) + + it('should not match when collection name does not contain search term', () => { + const nft = createMockNft({ collectionName: 'Bored Ape Yacht Club' }) + expect(filterNft(nft, 'CryptoPunks')).toBe(false) + }) + + it('should handle undefined collection name', () => { + const nft = createMockNft({ collectionName: undefined }) + expect(filterNft(nft, 'Yacht')).toBe(false) + }) + }) + + describe('when searching by token ID', () => { + it('should match exact token ID', () => { + const nft = createMockNft({ tokenId: '1234' }) + expect(filterNft(nft, '1234')).toBe(true) + }) + + it('should match partial token ID', () => { + const nft = createMockNft({ tokenId: '1234' }) + expect(filterNft(nft, '123')).toBe(true) + }) + + it('should be case-insensitive', () => { + const nft = createMockNft({ tokenId: 'ABC123' }) + expect(filterNft(nft, 'abc')).toBe(true) + expect(filterNft(nft, 'ABC')).toBe(true) + expect(filterNft(nft, 'AbC')).toBe(true) + }) + + it('should not match when token ID does not contain search term', () => { + const nft = createMockNft({ tokenId: '1234' }) + expect(filterNft(nft, '5678')).toBe(false) + }) + + it('should handle undefined token ID', () => { + const nft = createMockNft({ + tokenId: undefined, + name: undefined, + collectionName: undefined, + contractAddress: undefined, + }) + expect(filterNft(nft, '1234')).toBe(false) + }) + }) + + describe('when searching by contract address', () => { + it('should match exact contract address', () => { + const nft = createMockNft({ contractAddress: '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D' }) + expect(filterNft(nft, '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D')).toBe(true) + }) + + it('should match partial contract address', () => { + const nft = createMockNft({ contractAddress: '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D' }) + expect(filterNft(nft, 'BC4CA0')).toBe(true) + }) + + it('should be case-insensitive', () => { + const nft = createMockNft({ contractAddress: '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D' }) + expect(filterNft(nft, 'bc4ca0')).toBe(true) + expect(filterNft(nft, 'BC4CA0')).toBe(true) + expect(filterNft(nft, 'Bc4Ca0')).toBe(true) + }) + + it('should not match when contract address does not contain search term', () => { + const nft = createMockNft({ contractAddress: '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D' }) + expect(filterNft(nft, '0x123456789')).toBe(false) + }) + + it('should handle undefined contract address', () => { + const nft = createMockNft({ contractAddress: undefined }) + expect(filterNft(nft, 'BC4CA0')).toBe(false) + }) + }) + + describe('when searching with whitespace', () => { + it('should trim leading and trailing whitespace', () => { + const nft = createMockNft({ name: 'Bored Ape #1234' }) + expect(filterNft(nft, ' Bored ')).toBe(true) + expect(filterNft(nft, '\tBored\n')).toBe(true) + }) + + it('should handle whitespace-only search as empty search', () => { + const nft = createMockNft() + expect(filterNft(nft, ' ')).toBe(true) + expect(filterNft(nft, '\t\n')).toBe(true) + }) + }) + + describe('edge cases', () => { + it('should handle NFT with all undefined fields', () => { + const nft = createMockNft({ + name: undefined, + collectionName: undefined, + tokenId: undefined, + contractAddress: undefined, + }) + expect(filterNft(nft, 'anything')).toBe(false) + }) + + it('should handle NFT with empty string fields', () => { + const nft = createMockNft({ + name: '', + collectionName: '', + tokenId: '', + contractAddress: '', + }) + expect(filterNft(nft, 'anything')).toBe(false) + }) + + it('should handle special characters in search', () => { + const nft = createMockNft({ name: 'NFT #1234' }) + expect(filterNft(nft, '#')).toBe(true) + expect(filterNft(nft, '1234')).toBe(true) + }) + + it('should handle unicode characters', () => { + const nft = createMockNft({ name: '🚀 Rocket NFT' }) + expect(filterNft(nft, '🚀')).toBe(true) + expect(filterNft(nft, 'Rocket')).toBe(true) + }) + }) + + describe('real-world examples', () => { + it('should match Bored Ape Yacht Club NFT', () => { + const nft = createMockNft({ + name: 'Bored Ape #1234', + collectionName: 'Bored Ape Yacht Club', + tokenId: '1234', + contractAddress: '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D', + }) + + expect(filterNft(nft, 'bored')).toBe(true) + expect(filterNft(nft, 'ape')).toBe(true) + expect(filterNft(nft, 'yacht')).toBe(true) + expect(filterNft(nft, '1234')).toBe(true) + expect(filterNft(nft, 'BC4CA0')).toBe(true) + }) + + it('should match CryptoPunks NFT', () => { + const nft = createMockNft({ + name: 'CryptoPunk #1234', + collectionName: 'CryptoPunks', + tokenId: '1234', + contractAddress: '0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB', + }) + + expect(filterNft(nft, 'crypto')).toBe(true) + expect(filterNft(nft, 'punk')).toBe(true) + expect(filterNft(nft, 'punks')).toBe(true) + expect(filterNft(nft, '1234')).toBe(true) + }) + + it('should not match unrelated NFTs', () => { + const nft = createMockNft({ + name: 'Bored Ape #1234', + collectionName: 'Bored Ape Yacht Club', + tokenId: '1234', + contractAddress: '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D', + }) + + expect(filterNft(nft, 'cryptopunk')).toBe(false) + expect(filterNft(nft, 'azuki')).toBe(false) + expect(filterNft(nft, '5678')).toBe(false) + }) + }) +}) diff --git a/apps/web/src/pages/Portfolio/NFTs/utils/filterNfts.ts b/apps/web/src/pages/Portfolio/NFTs/utils/filterNfts.ts new file mode 100644 index 00000000000..643712033f0 --- /dev/null +++ b/apps/web/src/pages/Portfolio/NFTs/utils/filterNfts.ts @@ -0,0 +1,32 @@ +import { NFTItem } from 'uniswap/src/features/nfts/types' + +/** + * Filters an NFT item based on a search query. + * The search is case-insensitive and matches against: + * - NFT name + * - Collection name + * - Token ID + * - Contract address + * + * @param item - The NFT item to filter + * @param searchQuery - The search query (will be converted to lowercase) + * @returns true if the item matches the search query, false otherwise + */ +export function filterNft(item: NFTItem, searchQuery: string): boolean { + if (!searchQuery.trim()) { + return true + } + + const lowercaseSearch = searchQuery.trim().toLowerCase() + const name = item.name?.toLowerCase() ?? '' + const collectionName = item.collectionName?.toLowerCase() ?? '' + const tokenId = item.tokenId?.toLowerCase() ?? '' + const contract = item.contractAddress?.toLowerCase() ?? '' + + return ( + name.includes(lowercaseSearch) || + collectionName.includes(lowercaseSearch) || + tokenId.includes(lowercaseSearch) || + contract.includes(lowercaseSearch) + ) +} diff --git a/apps/web/src/pages/Portfolio/Nfts.tsx b/apps/web/src/pages/Portfolio/Nfts.tsx deleted file mode 100644 index 49fdcff4e64..00000000000 --- a/apps/web/src/pages/Portfolio/Nfts.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { useTranslation } from 'react-i18next' -import { Flex, Text } from 'ui/src' -import { InterfacePageName } from 'uniswap/src/features/telemetry/constants' -import Trace from 'uniswap/src/features/telemetry/Trace' - -export default function PortfolioNfts() { - const { t } = useTranslation() - - return ( - - - {t('portfolio.nfts.title')} - - Coming Soon - - This feature is under development and will be available soon. - - - - - ) -} diff --git a/apps/web/src/pages/Portfolio/Portfolio.tsx b/apps/web/src/pages/Portfolio/Portfolio.tsx index b58e47ab710..8959a955b19 100644 --- a/apps/web/src/pages/Portfolio/Portfolio.tsx +++ b/apps/web/src/pages/Portfolio/Portfolio.tsx @@ -1,70 +1,59 @@ -import { useAccount } from 'hooks/useAccount' -import PortfolioActivity from 'pages/Portfolio/Activity/Activity' -import PortfolioConnectWalletView from 'pages/Portfolio/ConnectWalletView' -import PortfolioDefi from 'pages/Portfolio/Defi' +import { Layers, PortfolioDisconnectedDemoViewProperties, useExperimentValueFromLayer } from '@universe/gating' +import PortfolioConnectWalletBanner from 'pages/Portfolio/ConnectWalletBanner' +import { ConnectWalletBottomOverlay } from 'pages/Portfolio/ConnectWalletBottomOverlay' import PortfolioHeader from 'pages/Portfolio/Header/Header' -import { usePortfolioParams } from 'pages/Portfolio/Header/hooks/usePortfolioParams' -import { usePortfolioTabsAnimation } from 'pages/Portfolio/Header/hooks/usePortfolioTabsAnimation' -import PortfolioNfts from 'pages/Portfolio/Nfts' -import PortfolioOverview from 'pages/Portfolio/Overview' -import PortfolioTokens from 'pages/Portfolio/Tokens/Tokens' -import { PortfolioTab } from 'pages/Portfolio/types' -import { useLocation } from 'react-router' +import useIsConnected from 'pages/Portfolio/Header/hooks/useIsConnected' +import { PortfolioContent } from 'pages/Portfolio/PortfolioContent' +import PortfolioDisconnectedView from 'pages/Portfolio/PortfolioDisconnectedView' import { Flex } from 'ui/src' -import { TransitionItem } from 'ui/src/animations/components/AnimatePresencePager' import { InterfacePageName } from 'uniswap/src/features/telemetry/constants' import Trace from 'uniswap/src/features/telemetry/Trace' -const renderPortfolioContent = (tab: PortfolioTab | undefined) => { - switch (tab) { - case PortfolioTab.Overview: - return - case PortfolioTab.Tokens: - return - case PortfolioTab.Defi: - return - case PortfolioTab.Nfts: - return - case PortfolioTab.Activity: - return - default: - return - } -} - // eslint-disable-next-line import/no-unused-modules -- used in RouteDefinitions.tsx via lazy import export default function Portfolio() { - const { pathname } = useLocation() - const account = useAccount() - const animationType = usePortfolioTabsAnimation(pathname) - const { tab } = usePortfolioParams() + const isConnected = useIsConnected() + const showDemoView = useExperimentValueFromLayer({ + layerName: Layers.PortfolioPage, + param: PortfolioDisconnectedDemoViewProperties.DemoViewEnabled, + defaultValue: false, + }) return ( - - {account.address ? ( - <> - + {!showDemoView && !isConnected ? ( + + ) : ( + + {!isConnected && } + {!isConnected && } + + {isConnected ? ( + <> + + + {/* Animated Content Area - All routes show same content, filtered by chain */} + + + ) : ( + <> + - {/* Animated Content Area - All routes show same content, filtered by chain */} - - - {renderPortfolioContent(tab)} - - - - ) : ( - - )} - + {/* Animated Content Area - All routes show same content, filtered by chain */} + + + + + )} + + )} ) } diff --git a/apps/web/src/pages/Portfolio/PortfolioContent.tsx b/apps/web/src/pages/Portfolio/PortfolioContent.tsx new file mode 100644 index 00000000000..9f859aaa643 --- /dev/null +++ b/apps/web/src/pages/Portfolio/PortfolioContent.tsx @@ -0,0 +1,42 @@ +import PortfolioActivity from 'pages/Portfolio/Activity/Activity' +import PortfolioDefi from 'pages/Portfolio/Defi' +import { usePortfolioParams } from 'pages/Portfolio/Header/hooks/usePortfolioParams' +import { usePortfolioTabsAnimation } from 'pages/Portfolio/Header/hooks/usePortfolioTabsAnimation' +import PortfolioNfts from 'pages/Portfolio/NFTs/Nfts' +import PortfolioOverview from 'pages/Portfolio/Overview' +import PortfolioTokens from 'pages/Portfolio/Tokens/Tokens' +import { PortfolioTab } from 'pages/Portfolio/types' +import { useLocation } from 'react-router' +import { Flex } from 'ui/src' +import { TransitionItem } from 'ui/src/animations/components/AnimatePresencePager' + +const renderPortfolioContent = (tab: PortfolioTab | undefined) => { + switch (tab) { + case PortfolioTab.Overview: + return + case PortfolioTab.Tokens: + return + case PortfolioTab.Defi: + return + case PortfolioTab.Nfts: + return + case PortfolioTab.Activity: + return + default: + return + } +} + +export function PortfolioContent({ disabled }: { disabled?: boolean }): JSX.Element { + const { pathname } = useLocation() + const animationType = usePortfolioTabsAnimation(pathname) + const { tab } = usePortfolioParams() + + return ( + + + {renderPortfolioContent(tab)} + + + ) +} diff --git a/apps/web/src/pages/Portfolio/PortfolioDisconnectedView.tsx b/apps/web/src/pages/Portfolio/PortfolioDisconnectedView.tsx new file mode 100644 index 00000000000..fe9e4c354d6 --- /dev/null +++ b/apps/web/src/pages/Portfolio/PortfolioDisconnectedView.tsx @@ -0,0 +1,105 @@ +import DISCONNECTED_B_DARK from 'assets/images/portfolio-page-promo/dark.svg' +import DISCONNECTED_B_LIGHT from 'assets/images/portfolio-page-promo/light.svg' +import { useAccountDrawer } from 'components/AccountDrawer/MiniPortfolio/hooks' +import { useTranslation } from 'react-i18next' +import { Button, Flex, Image, Text, useIsDarkMode, useSporeColors } from 'ui/src' +import { INTERFACE_NAV_HEIGHT } from 'ui/src/theme' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' + +const PADDING_TOP = 60 +const NAV_BORDER_WIDTH = 1 +const OFFSET_TOP = INTERFACE_NAV_HEIGHT + NAV_BORDER_WIDTH +const LEFT_CONTENT_MAX_WIDTH = 262 + +export default function PortfolioDisconnectedView() { + const { t } = useTranslation() + const enabledChains = useEnabledChains() + const isDarkMode = useIsDarkMode() + const accountDrawer = useAccountDrawer() + const colors = useSporeColors() + + return ( + + + + + {t('common.getStarted')} + + + {t('portfolio.disconnected.cta.description', { numNetworks: enabledChains.chains.length })} + + + + + + + + + + + ) +} diff --git a/apps/web/src/pages/Portfolio/Tokens/Table/TokensContextMenuWrapper.tsx b/apps/web/src/pages/Portfolio/Tokens/Table/TokensContextMenuWrapper.tsx index ae185fdcc14..3bd16b1bd30 100644 --- a/apps/web/src/pages/Portfolio/Tokens/Table/TokensContextMenuWrapper.tsx +++ b/apps/web/src/pages/Portfolio/Tokens/Table/TokensContextMenuWrapper.tsx @@ -1,3 +1,4 @@ +import useIsConnected from 'pages/Portfolio/Header/hooks/useIsConnected' import { TokenData } from 'pages/Portfolio/Tokens/hooks/useTransformTokenTableData' import { PropsWithChildren, useMemo } from 'react' import { ContextMenuTriggerMode } from 'uniswap/src/components/menus/types' @@ -9,6 +10,7 @@ export default function TokensContextMenuWrapper({ triggerMode, children, }: PropsWithChildren<{ tokenData: TokenData; triggerMode?: ContextMenuTriggerMode }>): React.ReactNode { + const isConnected = useIsConnected() const portfolioBalance: PortfolioBalance | undefined = useMemo(() => { if (!tokenData.currencyInfo) { return undefined @@ -25,7 +27,7 @@ export default function TokensContextMenuWrapper({ } }, [tokenData.currencyInfo, tokenData.id, tokenData.balance.value, tokenData.change1d, tokenData.rawValue]) - if (!portfolioBalance) { + if (!portfolioBalance || !isConnected) { return children } diff --git a/apps/web/src/pages/Portfolio/Tokens/Table/TokensTable.tsx b/apps/web/src/pages/Portfolio/Tokens/Table/TokensTable.tsx new file mode 100644 index 00000000000..39c41307320 --- /dev/null +++ b/apps/web/src/pages/Portfolio/Tokens/Table/TokensTable.tsx @@ -0,0 +1,56 @@ +import { NetworkStatus } from '@apollo/client' +import { TokenData } from 'pages/Portfolio/Tokens/hooks/useTransformTokenTableData' +import TokensTableInner from 'pages/Portfolio/Tokens/Table/TokensTableInner' +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { ScrollSync } from 'react-scroll-sync' +import { Flex, HeightAnimator, Text, TouchableArea } from 'ui/src' +import { AnglesDownUp } from 'ui/src/components/icons/AnglesDownUp' +import { SortVertical } from 'ui/src/components/icons/SortVertical' + +interface TokensTableProps { + visible: TokenData[] + hidden: TokenData[] + loading: boolean + refetching?: boolean + networkStatus: NetworkStatus + error?: Error | undefined +} + +export default function TokensTable({ visible, hidden, loading, refetching, networkStatus, error }: TokensTableProps) { + const { t } = useTranslation() + const [isOpen, setIsOpen] = useState(false) + const tableLoading = loading && !refetching + + return ( + // Scroll Sync Architecture: + // - Outer ScrollSync coordinates horizontal scrolling between visible and hidden tables + // - Each TokensTableInner uses externalScrollSync=true to skip its own ScrollSync wrapper + // - Both tables use ScrollSyncPane with scrollGroup="portfolio-tokens" for coordination + // - DO NOT remove this outer ScrollSync wrapper without updating the Table components + + + + {hidden.length > 0 && ( + <> + setIsOpen(!isOpen)} row gap="$gap8" p="$spacing16"> + + {t('hidden.tokens.info.text.button', { numHidden: hidden.length })} + + + {isOpen ? ( + + ) : ( + + )} + + + + + + + )} + + + ) +} diff --git a/apps/web/src/pages/Portfolio/Tokens/Table/Table.tsx b/apps/web/src/pages/Portfolio/Tokens/Table/TokensTableInner.tsx similarity index 66% rename from apps/web/src/pages/Portfolio/Tokens/Table/Table.tsx rename to apps/web/src/pages/Portfolio/Tokens/Table/TokensTableInner.tsx index e7f13d1fd33..aeb4600af6a 100644 --- a/apps/web/src/pages/Portfolio/Tokens/Table/Table.tsx +++ b/apps/web/src/pages/Portfolio/Tokens/Table/TokensTableInner.tsx @@ -15,12 +15,29 @@ import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { Text } from 'ui/src' -export default function TokensTable({ tokenData }: { tokenData: TokenData[] }) { +const hasRow = (obj: unknown): obj is { row: { original: T } } => { + const maybeRow = (obj as { row?: unknown }).row + return typeof maybeRow === 'object' && maybeRow !== null && 'original' in maybeRow && maybeRow.original !== undefined +} + +export default function TokensTableInner({ + tokenData, + hideHeader, + loading = false, + error, +}: { + tokenData: TokenData[] + hideHeader?: boolean + loading?: boolean + error?: Error | undefined +}) { const { t } = useTranslation() + const showLoadingSkeleton = loading || !!error // Create table columns const columns = useMemo(() => { const columnHelper = createColumnHelper() + return [ columnHelper.accessor('currencyInfo', { header: () => ( @@ -31,10 +48,9 @@ export default function TokensTable({ tokenData }: { tokenData: TokenData[] }) { ), cell: (info) => { - const currencyInfo = info.getValue() return ( - - + + ) }, @@ -48,10 +64,9 @@ export default function TokensTable({ tokenData }: { tokenData: TokenData[] }) { ), cell: (info) => { - const value = info.getValue() return ( - - + + ) }, @@ -65,10 +80,9 @@ export default function TokensTable({ tokenData }: { tokenData: TokenData[] }) { ), cell: (info) => { - const value = info.getValue() return ( - - + + ) }, @@ -82,10 +96,9 @@ export default function TokensTable({ tokenData }: { tokenData: TokenData[] }) { ), cell: (info) => { - const value = info.getValue() return ( - - + + ) }, @@ -99,11 +112,9 @@ export default function TokensTable({ tokenData }: { tokenData: TokenData[] }) { ), cell: (info) => { - const value = info.getValue() - return ( - - + + ) }, @@ -117,10 +128,9 @@ export default function TokensTable({ tokenData }: { tokenData: TokenData[] }) { ), cell: (info) => { - const value = info.getValue() return ( - - + + ) }, @@ -130,28 +140,33 @@ export default function TokensTable({ tokenData }: { tokenData: TokenData[] }) { size: 40, header: () => , cell: (info) => { - const tokenData = info.row.original + const tokenData = hasRow(info) ? info.row.original : undefined return ( - - + + {tokenData && } ) }, }), ] - }, [t]) + }, [t, showLoadingSkeleton]) return (
row.id} - rowWrapper={(row, content) => ( - {content} - )} + rowWrapper={ + loading + ? undefined + : (row, content) => {content} + } /> ) } diff --git a/apps/web/src/pages/Portfolio/Tokens/Table/columns/Balance.tsx b/apps/web/src/pages/Portfolio/Tokens/Table/columns/Balance.tsx index 8a0d4a018f0..9a9a00ac191 100644 --- a/apps/web/src/pages/Portfolio/Tokens/Table/columns/Balance.tsx +++ b/apps/web/src/pages/Portfolio/Tokens/Table/columns/Balance.tsx @@ -10,7 +10,7 @@ const Balance = memo(function Balance({ value, symbol }: TokenData['balance']) { } return ( - + {symbol} ) diff --git a/apps/web/src/pages/Portfolio/Tokens/Tokens.tsx b/apps/web/src/pages/Portfolio/Tokens/Tokens.tsx index 3b44943cf6f..4e44c419bcf 100644 --- a/apps/web/src/pages/Portfolio/Tokens/Tokens.tsx +++ b/apps/web/src/pages/Portfolio/Tokens/Tokens.tsx @@ -1,27 +1,122 @@ -import { useAccount } from 'hooks/useAccount' +import { SearchInput } from 'pages/Portfolio/components/SearchInput' +import { usePortfolioParams } from 'pages/Portfolio/Header/hooks/usePortfolioParams' +import { usePortfolioAddress } from 'pages/Portfolio/hooks/usePortfolioAddress' import { useTransformTokenTableData } from 'pages/Portfolio/Tokens/hooks/useTransformTokenTableData' -import TokensTable from 'pages/Portfolio/Tokens/Table/Table' import { TokensAllocationChart } from 'pages/Portfolio/Tokens/Table/TokensAllocationChart' -import { Flex } from 'ui/src' +import TokensTable from 'pages/Portfolio/Tokens/Table/TokensTable' +import { filterTokensBySearch } from 'pages/Portfolio/Tokens/utils/filterTokensBySearch' +import { memo, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { Flex, RemoveScroll, Text } from 'ui/src' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { PortfolioBalance } from 'uniswap/src/features/portfolio/PortfolioBalance/PortfolioBalance' import { InterfacePageName } from 'uniswap/src/features/telemetry/constants' import Trace from 'uniswap/src/features/telemetry/Trace' +import { parseChainFromTokenSearchQuery } from 'uniswap/src/utils/search/parseChainFromTokenSearchQuery' + +const TokenCountIndicator = memo(({ count }: { count: number }) => { + const { t } = useTranslation() + + return ( + + + + {t('portfolio.tokens.balance.totalTokens', { numTokens: count })} + + + ) +}) + +TokenCountIndicator.displayName = 'TokenCountIndicator' export default function PortfolioTokens() { - const account = useAccount() - const tokenData = useTransformTokenTableData() + const portfolioAddress = usePortfolioAddress() + const { t } = useTranslation() + const [search, setSearch] = useState('') + const { chains: enabledChains } = useEnabledChains() + const { chainId: urlChainId } = usePortfolioParams() + + // Parse search query to extract chain filter and search term + const { chainFilter, searchTerm } = useMemo(() => { + return parseChainFromTokenSearchQuery(search, enabledChains) + }, [search, enabledChains]) + + // Use URL chain ID as primary filter, search chain filter as fallback + const effectiveChainId = urlChainId || chainFilter + + // Get token data filtered by chain at API level + const { + visible: tokenData, + hidden: hiddenTokenData, + loading, + refetching, + networkStatus, + error, + } = useTransformTokenTableData({ + chainIds: effectiveChainId ? [effectiveChainId] : undefined, + }) + + // Filter tokens by search term at client level (chain filtering is handled at API level) + const filteredTokenData = useMemo(() => { + return filterTokensBySearch({ tokens: tokenData || [], searchTerm }) + // return filterTokensBySearch({ tokens: tokenData, searchTerm }) || [] + }, [tokenData, searchTerm]) + + const filteredHiddenTokenData = useMemo(() => { + return filterTokensBySearch({ tokens: hiddenTokenData || [], searchTerm }) || [] + }, [hiddenTokenData, searchTerm]) return ( - - {account.address && ( + + - - - + + : undefined} + /> + + - + {(tokenData && tokenData.length > 0) || loading ? ( + <> + + {(filteredTokenData?.length ?? 0) > 0 || loading ? ( + - )} - + + ) } diff --git a/apps/web/src/pages/Portfolio/Tokens/hooks/useTransformTokenTableData.ts b/apps/web/src/pages/Portfolio/Tokens/hooks/useTransformTokenTableData.ts index 082c0562e57..2812331fa28 100644 --- a/apps/web/src/pages/Portfolio/Tokens/hooks/useTransformTokenTableData.ts +++ b/apps/web/src/pages/Portfolio/Tokens/hooks/useTransformTokenTableData.ts @@ -1,6 +1,9 @@ -import { useAccount } from 'hooks/useAccount' +import { NetworkStatus } from '@apollo/client' +import { usePortfolioAddress } from 'pages/Portfolio/hooks/usePortfolioAddress' import { useMemo } from 'react' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { useSortedPortfolioBalances } from 'uniswap/src/features/dataApi/balances/balances' +import type { PortfolioBalance } from 'uniswap/src/features/dataApi/types' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext' import { NumberType } from 'utilities/src/format/types' @@ -20,41 +23,54 @@ export interface TokenData { } // Custom hook to format portfolio data -export function useTransformTokenTableData(): TokenData[] { - const account = useAccount() - const { data: portfolioData, loading } = useSortedPortfolioBalances({ - evmAddress: account.address || undefined, - }) +export function useTransformTokenTableData({ chainIds }: { chainIds?: UniverseChainId[] }): { + visible: TokenData[] | null + hidden: TokenData[] | null + loading: boolean + refetching: boolean + error: Error | undefined + refetch: (() => void) | undefined + networkStatus: NetworkStatus +} { + const portfolioAddress = usePortfolioAddress() const { convertFiatAmountFormatted, formatNumberOrString } = useLocalizationContext() + const { + data: sortedBalances, + loading, + error, + refetch, + networkStatus, + } = useSortedPortfolioBalances({ + evmAddress: portfolioAddress, + chainIds, + }) + return useMemo(() => { - if (!account.address || !portfolioData?.balances || loading) { - return [] + // Only show empty state on initial load, not during refetch + const isInitialLoading = loading && !sortedBalances + const isRefetching = loading && !!sortedBalances + + if (isInitialLoading) { + return { visible: null, hidden: null, loading, refetching: false, error, refetch, networkStatus } + } + + if (!sortedBalances) { + return { visible: [], hidden: [], loading, refetching: false, error, refetch, networkStatus } } // Compute total USD across visible balances to determine allocation per token - const totalUSD = portfolioData.balances.reduce((sum, b) => sum + (b.balanceUSD ?? 0), 0) + const totalUSDVisible = sortedBalances.balances.reduce((sum, b) => sum + (b.balanceUSD ?? 0), 0) - return portfolioData.balances.map((balance) => { - // Format price (using balanceUSD / quantity for now, could be improved with actual price data) + const mapBalanceToTokenData = (balance: PortfolioBalance, allocationFromTotal?: number): TokenData => { const price = balance.balanceUSD && balance.quantity > 0 ? convertFiatAmountFormatted(balance.balanceUSD / balance.quantity, NumberType.FiatTokenPrice) : '$0.00' - // Format balance quantity - const formattedBalance = formatNumberOrString({ - value: balance.quantity, - type: NumberType.TokenNonTx, - }) - - // Format USD value + const formattedBalance = formatNumberOrString({ value: balance.quantity, type: NumberType.TokenNonTx }) const value = convertFiatAmountFormatted(balance.balanceUSD, NumberType.PortfolioBalance) - // Allocation percentage of this token vs total portfolio USD (0..100) - const balanceUSD = balance.balanceUSD ?? 0 - const allocation = totalUSD > 0 ? (balanceUSD / totalUSD) * 100 : 0 - return { id: balance.id, currencyInfo: balance.currencyInfo, @@ -66,8 +82,18 @@ export function useTransformTokenTableData(): TokenData[] { }, value, rawValue: balance.balanceUSD, - allocation, + allocation: allocationFromTotal ?? 0, } + } + + const visible = sortedBalances.balances.map((b) => { + const balanceUSD = b.balanceUSD ?? 0 + const allocation = totalUSDVisible > 0 ? (balanceUSD / totalUSDVisible) * 100 : 0 + return mapBalanceToTokenData(b, allocation) }) - }, [account.address, portfolioData?.balances, loading, convertFiatAmountFormatted, formatNumberOrString]) + + const hidden = sortedBalances.hiddenBalances.map((b) => mapBalanceToTokenData(b, 0)) + + return { visible, hidden, loading, refetching: isRefetching, refetch, networkStatus, error } + }, [loading, sortedBalances, convertFiatAmountFormatted, formatNumberOrString, error, refetch, networkStatus]) } diff --git a/apps/web/src/pages/Portfolio/Tokens/utils/filterTokensBySearch.test.ts b/apps/web/src/pages/Portfolio/Tokens/utils/filterTokensBySearch.test.ts new file mode 100644 index 00000000000..ab64034b7bb --- /dev/null +++ b/apps/web/src/pages/Portfolio/Tokens/utils/filterTokensBySearch.test.ts @@ -0,0 +1,280 @@ +import { Currency } from '@uniswap/sdk-core' +import { filterTokensBySearch } from 'pages/Portfolio/Tokens/utils/filterTokensBySearch' +import { TEST_TOKEN_1 } from 'test-utils/constants' + +// Mock the doesTokenMatchSearchTerm function to have full control over test scenarios +vi.mock('uniswap/src/utils/search/doesTokenMatchSearchTerm', () => ({ + doesTokenMatchSearchTerm: vi.fn(), +})) + +import { doesTokenMatchSearchTerm } from 'uniswap/src/utils/search/doesTokenMatchSearchTerm' + +const mockDoesTokenMatchSearchTerm = vi.mocked(doesTokenMatchSearchTerm) + +// Test data factory functions using test tokens +const createMockCurrencyInfo = ( + overrides: Partial<{ currencyId: string; currency: Currency }> = {}, +): { currencyId: string; currency: Currency } => ({ + currencyId: 'TEST', + currency: TEST_TOKEN_1, // Default to TEST_TOKEN_1 + ...overrides, +}) + +const createMockTokenWithInfo = ( + overrides: Partial<{ currencyInfo: { currencyId: string; currency: Currency } | null }> = {}, +): { currencyInfo: { currencyId: string; currency: Currency } | null } => ({ + currencyInfo: createMockCurrencyInfo(), + ...overrides, +}) + +describe('filterTokensBySearch', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('when searchTerm is empty or undefined', () => { + it('should return all tokens when searchTerm is undefined', () => { + const tokens = [createMockTokenWithInfo(), createMockTokenWithInfo()] + + const result = filterTokensBySearch({ + tokens, + searchTerm: undefined, + }) + + expect(result).toBe(tokens) + expect(mockDoesTokenMatchSearchTerm).not.toHaveBeenCalled() + }) + + it('should return all tokens when searchTerm is null', () => { + const tokens = [createMockTokenWithInfo(), createMockTokenWithInfo()] + + const result = filterTokensBySearch({ + tokens, + searchTerm: null, + }) + + expect(result).toBe(tokens) + expect(mockDoesTokenMatchSearchTerm).not.toHaveBeenCalled() + }) + + it('should return all tokens when searchTerm is empty string', () => { + const tokens = [createMockTokenWithInfo(), createMockTokenWithInfo()] + + const result = filterTokensBySearch({ + tokens, + searchTerm: '', + }) + + expect(result).toBe(tokens) + expect(mockDoesTokenMatchSearchTerm).not.toHaveBeenCalled() + }) + + it('should return all tokens when searchTerm is only whitespace', () => { + const tokens = [createMockTokenWithInfo(), createMockTokenWithInfo()] + + const result = filterTokensBySearch({ + tokens, + searchTerm: ' ', + }) + + expect(result).toBe(tokens) + expect(mockDoesTokenMatchSearchTerm).not.toHaveBeenCalled() + }) + }) + + describe('when tokens array is undefined', () => { + it('should return undefined when tokens is undefined', () => { + const result = filterTokensBySearch({ + tokens: undefined, + searchTerm: 'test', + }) + + expect(result).toBeUndefined() + expect(mockDoesTokenMatchSearchTerm).not.toHaveBeenCalled() + }) + }) + + describe('when tokens array is empty', () => { + it('should return empty array when tokens is empty', () => { + const result = filterTokensBySearch({ + tokens: [], + searchTerm: 'test', + }) + + expect(result).toEqual([]) + expect(mockDoesTokenMatchSearchTerm).not.toHaveBeenCalled() + }) + }) + + describe('when filtering with valid search term', () => { + it('should filter tokens based on doesTokenMatchSearchTerm results', () => { + const token1 = createMockTokenWithInfo() + const token2 = createMockTokenWithInfo() + const token3 = createMockTokenWithInfo() + const tokens = [token1, token2, token3] + + // Mock the search function to return different results for each token + mockDoesTokenMatchSearchTerm + .mockReturnValueOnce(true) // token1 matches + .mockReturnValueOnce(false) // token2 doesn't match + .mockReturnValueOnce(true) // token3 matches + + const result = filterTokensBySearch({ + tokens, + searchTerm: 'test', + }) + + expect(result).toEqual([token1, token3]) + expect(mockDoesTokenMatchSearchTerm).toHaveBeenCalledTimes(3) + expect(mockDoesTokenMatchSearchTerm).toHaveBeenCalledWith(token1, 'test') + expect(mockDoesTokenMatchSearchTerm).toHaveBeenCalledWith(token2, 'test') + expect(mockDoesTokenMatchSearchTerm).toHaveBeenCalledWith(token3, 'test') + }) + + it('should return empty array when no tokens match', () => { + const token1 = createMockTokenWithInfo() + const token2 = createMockTokenWithInfo() + const tokens = [token1, token2] + + mockDoesTokenMatchSearchTerm.mockReturnValueOnce(false).mockReturnValueOnce(false) + + const result = filterTokensBySearch({ + tokens, + searchTerm: 'nonexistent', + }) + + expect(result).toEqual([]) + expect(mockDoesTokenMatchSearchTerm).toHaveBeenCalledTimes(2) + }) + + it('should return all tokens when all tokens match', () => { + const token1 = createMockTokenWithInfo() + const token2 = createMockTokenWithInfo() + const tokens = [token1, token2] + + mockDoesTokenMatchSearchTerm.mockReturnValueOnce(true).mockReturnValueOnce(true) + + const result = filterTokensBySearch({ + tokens, + searchTerm: 'common', + }) + + expect(result).toEqual(tokens) + expect(mockDoesTokenMatchSearchTerm).toHaveBeenCalledTimes(2) + }) + }) + + describe('with different token types', () => { + it('should work with tokens that have currencyInfo', () => { + const token = createMockTokenWithInfo({ + currencyInfo: createMockCurrencyInfo({ + currencyId: 'ABC', + currency: TEST_TOKEN_1, // Use TEST_TOKEN_1 (symbol: 'ABC', name: 'Abc') + }), + }) + + mockDoesTokenMatchSearchTerm.mockReturnValue(true) + + const result = filterTokensBySearch({ + tokens: [token], + searchTerm: 'abc', + }) + + expect(result).toEqual([token]) + expect(mockDoesTokenMatchSearchTerm).toHaveBeenCalledWith(token, 'abc') + }) + + it('should work with tokens that have null currencyInfo', () => { + const token = createMockTokenWithInfo({ + currencyInfo: null, + }) + + mockDoesTokenMatchSearchTerm.mockReturnValue(false) + + const result = filterTokensBySearch({ + tokens: [token], + searchTerm: 'test', + }) + + expect(result).toEqual([]) + expect(mockDoesTokenMatchSearchTerm).toHaveBeenCalledWith(token, 'test') + }) + }) + + describe('edge cases', () => { + it('should handle single token array', () => { + const token = createMockTokenWithInfo() + mockDoesTokenMatchSearchTerm.mockReturnValue(true) + + const result = filterTokensBySearch({ + tokens: [token], + searchTerm: 'test', + }) + + expect(result).toEqual([token]) + expect(mockDoesTokenMatchSearchTerm).toHaveBeenCalledTimes(1) + }) + + it('should preserve original array reference when no filtering occurs', () => { + const tokens = [createMockTokenWithInfo()] + + const result = filterTokensBySearch({ + tokens, + searchTerm: undefined, + }) + + expect(result).toBe(tokens) + }) + + it('should handle search term with special characters', () => { + const token = createMockTokenWithInfo() + mockDoesTokenMatchSearchTerm.mockReturnValue(true) + + const result = filterTokensBySearch({ + tokens: [token], + searchTerm: 'test@#$%^&*()', + }) + + expect(result).toEqual([token]) + expect(mockDoesTokenMatchSearchTerm).toHaveBeenCalledWith(token, 'test@#$%^&*()') + }) + + it('should handle very long search terms', () => { + const token = createMockTokenWithInfo() + const longSearchTerm = 'a'.repeat(1000) + mockDoesTokenMatchSearchTerm.mockReturnValue(false) + + const result = filterTokensBySearch({ + tokens: [token], + searchTerm: longSearchTerm, + }) + + expect(result).toEqual([]) + expect(mockDoesTokenMatchSearchTerm).toHaveBeenCalledWith(token, longSearchTerm) + }) + }) + + describe('type safety', () => { + it('should work with generic token types', () => { + interface ExtendedToken { + currencyInfo: { currencyId: string; currency: Currency } | null + customProperty: string + } + + const extendedToken: ExtendedToken = { + currencyInfo: createMockCurrencyInfo(), + customProperty: 'test', + } + + mockDoesTokenMatchSearchTerm.mockReturnValue(true) + + const result = filterTokensBySearch({ + tokens: [extendedToken], + searchTerm: 'test', + }) + + expect(result).toEqual([extendedToken]) + expect(mockDoesTokenMatchSearchTerm).toHaveBeenCalledWith(extendedToken, 'test') + }) + }) +}) diff --git a/apps/web/src/pages/Portfolio/Tokens/utils/filterTokensBySearch.ts b/apps/web/src/pages/Portfolio/Tokens/utils/filterTokensBySearch.ts new file mode 100644 index 00000000000..7fa87992045 --- /dev/null +++ b/apps/web/src/pages/Portfolio/Tokens/utils/filterTokensBySearch.ts @@ -0,0 +1,28 @@ +import { Currency } from '@uniswap/sdk-core' +import { doesTokenMatchSearchTerm } from 'uniswap/src/utils/search/doesTokenMatchSearchTerm' + +/** + * Filters tokens based on search criteria (name, symbol, address, chain name). + * This is a pure utility function for client-side filtering. + * + * @param tokens - Array of tokens to filter + * @param searchTerm - Search term to match against + * @param enabledChains - Array of enabled chain IDs to search within + * @returns Filtered array of tokens that match the search criteria + */ +export function filterTokensBySearch({ + tokens, + searchTerm, +}: { + tokens: T[] | undefined + searchTerm: string | undefined | null +}): T[] | undefined { + const trimmedSearchTerm = searchTerm?.trim() + if (!trimmedSearchTerm) { + return tokens + } + + return tokens?.filter((token) => { + return doesTokenMatchSearchTerm(token, trimmedSearchTerm) + }) +} diff --git a/apps/web/src/pages/Portfolio/hooks/usePortfolioAddress.ts b/apps/web/src/pages/Portfolio/hooks/usePortfolioAddress.ts new file mode 100644 index 00000000000..b3909dc2a0d --- /dev/null +++ b/apps/web/src/pages/Portfolio/hooks/usePortfolioAddress.ts @@ -0,0 +1,13 @@ +/* eslint-disable-next-line no-restricted-imports, no-restricted-syntax */ +import { useAccount } from 'hooks/useAccount' + +// This is the address used for the disconnected demo view. It is only used in the disconnected state for the portfolio page. +const DEMO_WALLET_ADDRESS = '0x8796207d877194d97a2c360c041f13887896FC79' + +export function usePortfolioAddress() { + const account = useAccount() + if (!account.address) { + return DEMO_WALLET_ADDRESS + } + return account.address +} diff --git a/apps/web/src/pages/Positions/ClaimFees.anvil.e2e.test.ts b/apps/web/src/pages/Positions/ClaimFees.anvil.e2e.test.ts index 1d50f7b36b5..670e989ec5c 100644 --- a/apps/web/src/pages/Positions/ClaimFees.anvil.e2e.test.ts +++ b/apps/web/src/pages/Positions/ClaimFees.anvil.e2e.test.ts @@ -1,4 +1,4 @@ -import { getPosition } from '@uniswap/client-pools/dist/pools/v1/api-PoolsService_connectquery' +import { getPosition } from '@uniswap/client-data-api/dist/data/v1/api-DataApiService_connectquery' import { createExpectSingleTransaction } from 'playwright/anvil/transactions' import { expect, getTest } from 'playwright/fixtures' import { DEFAULT_TEST_GAS_LIMIT, stubTradingApiEndpoint } from 'playwright/fixtures/tradingApi' diff --git a/apps/web/src/pages/Positions/PositionPage.tsx b/apps/web/src/pages/Positions/PositionPage.tsx index 44a4eda09fb..5f923abfd40 100644 --- a/apps/web/src/pages/Positions/PositionPage.tsx +++ b/apps/web/src/pages/Positions/PositionPage.tsx @@ -1,7 +1,8 @@ import { BigNumber } from '@ethersproject/bignumber' -import { Position, PositionStatus, ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' +import { Position, PositionStatus, ProtocolVersion } from '@uniswap/client-data-api/dist/data/v1/poolTypes_pb' import { Currency, CurrencyAmount, Percent, Price } from '@uniswap/sdk-core' import { GraphQLApi } from '@universe/api' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { BreadcrumbNavContainer, BreadcrumbNavLink } from 'components/BreadcrumbNav' import { WrappedLiquidityPositionRangeChart } from 'components/Charts/LiquidityPositionRangeChart/LiquidityPositionRangeChart' import { Dropdown } from 'components/Dropdowns/Dropdown' @@ -56,8 +57,6 @@ import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' import { useSupportedChainId } from 'uniswap/src/features/chains/hooks/useSupportedChainId' import { EVMUniverseChainId, UniverseChainId } from 'uniswap/src/features/chains/types' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext' import { isEVMChain } from 'uniswap/src/features/platforms/utils/chains' import { InterfacePageName } from 'uniswap/src/features/telemetry/constants' diff --git a/apps/web/src/pages/Positions/TopPools.tsx b/apps/web/src/pages/Positions/TopPools.tsx index 18a5f69a9ff..92b14c1ebe2 100644 --- a/apps/web/src/pages/Positions/TopPools.tsx +++ b/apps/web/src/pages/Positions/TopPools.tsx @@ -2,6 +2,7 @@ import { PoolSortFields } from 'appGraphql/data/pools/useTopPools' import { OrderDirection } from 'appGraphql/data/util' import { ExploreStatsResponse } from '@uniswap/client-explore/dist/uniswap/explore/v1/service_pb' import { ALL_NETWORKS_ARG } from '@universe/api' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { ExternalArrowLink } from 'components/Liquidity/ExternalArrowLink' import { useAccount } from 'hooks/useAccount' import { TopPoolsSection } from 'pages/Positions/TopPoolsSection' @@ -10,8 +11,6 @@ import { useTopPools } from 'state/explore/topPools' import { Flex, useMedia } from 'ui/src' import { useExploreStatsQuery } from 'uniswap/src/data/rest/exploreStats' import { UniverseChainId } from 'uniswap/src/features/chains/types' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' const MAX_BOOSTED_POOLS = 3 diff --git a/apps/web/src/pages/Positions/V2PositionPage.tsx b/apps/web/src/pages/Positions/V2PositionPage.tsx index 40cf64e6e4e..1b79fdd6bc5 100644 --- a/apps/web/src/pages/Positions/V2PositionPage.tsx +++ b/apps/web/src/pages/Positions/V2PositionPage.tsx @@ -1,4 +1,5 @@ -import { ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' +import { ProtocolVersion } from '@uniswap/client-data-api/dist/data/v1/poolTypes_pb' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { BreadcrumbNavContainer, BreadcrumbNavLink } from 'components/BreadcrumbNav' import { useGetPoolTokenPercentage } from 'components/Liquidity/hooks/useGetPoolTokenPercentage' import { LiquidityPositionInfo, LiquidityPositionInfoLoader } from 'components/Liquidity/LiquidityPositionInfo' @@ -25,8 +26,6 @@ import { useGetPositionQuery } from 'uniswap/src/data/rest/getPosition' import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' import { useSupportedChainId } from 'uniswap/src/features/chains/hooks/useSupportedChainId' import { UniverseChainId } from 'uniswap/src/features/chains/types' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext' import { isEVMChain } from 'uniswap/src/features/platforms/utils/chains' import { useUSDCValue } from 'uniswap/src/features/transactions/hooks/useUSDCPrice' diff --git a/apps/web/src/pages/Positions/index.tsx b/apps/web/src/pages/Positions/index.tsx index 4fb2229256b..43c75054aeb 100644 --- a/apps/web/src/pages/Positions/index.tsx +++ b/apps/web/src/pages/Positions/index.tsx @@ -1,4 +1,5 @@ -import { PositionStatus, ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' +import { PositionStatus, ProtocolVersion } from '@uniswap/client-data-api/dist/data/v1/poolTypes_pb' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import PROVIDE_LIQUIDITY from 'assets/images/provideLiquidity.png' import tokenLogo from 'assets/images/token-logo.png' import V4_HOOK from 'assets/images/v4Hooks.png' @@ -34,8 +35,6 @@ import { uniswapUrls } from 'uniswap/src/constants/urls' import { useGetPositionsInfiniteQuery } from 'uniswap/src/data/rest/getPositions' import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { UniverseChainId } from 'uniswap/src/features/chains/types' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { Platform } from 'uniswap/src/features/platforms/types/Platform' import { InterfacePageName, UniswapEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' diff --git a/apps/web/src/pages/RemoveLiquidity/RemoveLiquidity.anvil.e2e.test.ts b/apps/web/src/pages/RemoveLiquidity/RemoveLiquidity.anvil.e2e.test.ts index 155481bbbbd..60cdd4784fb 100644 --- a/apps/web/src/pages/RemoveLiquidity/RemoveLiquidity.anvil.e2e.test.ts +++ b/apps/web/src/pages/RemoveLiquidity/RemoveLiquidity.anvil.e2e.test.ts @@ -1,4 +1,4 @@ -import { getPosition } from '@uniswap/client-pools/dist/pools/v1/api-PoolsService_connectquery' +import { getPosition } from '@uniswap/client-data-api/dist/data/v1/api-DataApiService_connectquery' import { ONE_MILLION_USDT } from 'playwright/anvil/utils' import { expect, getTest } from 'playwright/fixtures' import { stubTradingApiEndpoint } from 'playwright/fixtures/tradingApi' diff --git a/apps/web/src/pages/RemoveLiquidity/RemoveLiquidityModalContext.tsx b/apps/web/src/pages/RemoveLiquidity/RemoveLiquidityModalContext.tsx index 7dab6197615..0a558e17485 100644 --- a/apps/web/src/pages/RemoveLiquidity/RemoveLiquidityModalContext.tsx +++ b/apps/web/src/pages/RemoveLiquidity/RemoveLiquidityModalContext.tsx @@ -1,4 +1,4 @@ -import { ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' +import { ProtocolVersion } from '@uniswap/client-data-api/dist/data/v1/poolTypes_pb' import { Currency } from '@uniswap/sdk-core' import { getCurrencyWithOptionalUnwrap } from 'components/Liquidity/utils/currency' import { useModalInitialState } from 'hooks/useModalInitialState' diff --git a/apps/web/src/pages/RemoveLiquidity/RemoveLiquidityReview.tsx b/apps/web/src/pages/RemoveLiquidity/RemoveLiquidityReview.tsx index 4e219497812..5ad7f04a845 100644 --- a/apps/web/src/pages/RemoveLiquidity/RemoveLiquidityReview.tsx +++ b/apps/web/src/pages/RemoveLiquidity/RemoveLiquidityReview.tsx @@ -1,4 +1,4 @@ -import { ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' +import { ProtocolVersion } from '@uniswap/client-data-api/dist/data/v1/poolTypes_pb' import { CurrencyAmount } from '@uniswap/sdk-core' import { getLPBaseAnalyticsProperties } from 'components/Liquidity/analytics' import { useGetPoolTokenPercentage } from 'components/Liquidity/hooks/useGetPoolTokenPercentage' diff --git a/apps/web/src/pages/RemoveLiquidity/hooks/useRemoveLiquidityTxAndGasInfo.ts b/apps/web/src/pages/RemoveLiquidity/hooks/useRemoveLiquidityTxAndGasInfo.ts index c64e786d904..7ddeb195330 100644 --- a/apps/web/src/pages/RemoveLiquidity/hooks/useRemoveLiquidityTxAndGasInfo.ts +++ b/apps/web/src/pages/RemoveLiquidity/hooks/useRemoveLiquidityTxAndGasInfo.ts @@ -1,4 +1,4 @@ -import { ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' +import { ProtocolVersion } from '@uniswap/client-data-api/dist/data/v1/poolTypes_pb' import { TradingApi } from '@universe/api' import { getTokenOrZeroAddress } from 'components/Liquidity/utils/currency' import { getProtocolItems } from 'components/Liquidity/utils/protocolVersion' diff --git a/apps/web/src/pages/RouteDefinitions.tsx b/apps/web/src/pages/RouteDefinitions.tsx index 794e24ecd46..675c2e7fd5f 100644 --- a/apps/web/src/pages/RouteDefinitions.tsx +++ b/apps/web/src/pages/RouteDefinitions.tsx @@ -1,3 +1,4 @@ +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { getExploreDescription, getExploreTitle } from 'pages/getExploreTitle' import { getAddLiquidityPageTitle, getPositionPageDescription, getPositionPageTitle } from 'pages/getPositionPageTitle' // High-traffic pages (index and /swap) should not be lazy-loaded. @@ -7,8 +8,6 @@ import { lazy, ReactNode, Suspense, useMemo } from 'react' import { matchPath, Navigate, Route, Routes, useLocation } from 'react-router' import { CHROME_EXTENSION_UNINSTALL_URL_PATH } from 'uniswap/src/constants/urls' import { WRAPPED_SOL_ADDRESS_SOLANA } from 'uniswap/src/features/chains/svm/defaults' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { EXTENSION_PASSKEY_AUTH_PATH } from 'uniswap/src/features/passkey/constants' import i18n from 'uniswap/src/i18n' import { isBrowserRouterEnabled } from 'utils/env' diff --git a/apps/web/src/pages/Swap/Buy/Buy.e2e.test.ts b/apps/web/src/pages/Swap/Buy/Buy.e2e.test.ts index 5a725eb6dc7..2feb4d89169 100644 --- a/apps/web/src/pages/Swap/Buy/Buy.e2e.test.ts +++ b/apps/web/src/pages/Swap/Buy/Buy.e2e.test.ts @@ -1,5 +1,7 @@ import { expect, getTest } from 'playwright/fixtures' +import { stubTradingApiEndpoint } from 'playwright/fixtures/tradingApi' import { Mocks } from 'playwright/mocks/mocks' +import { uniswapUrls } from 'uniswap/src/constants/urls' import { TestID } from 'uniswap/src/test/fixtures/testIDs' const test = getTest() @@ -20,6 +22,7 @@ test.describe('Buy Crypto Form', () => { }) } + await stubTradingApiEndpoint({ page, endpoint: uniswapUrls.tradingApiPaths.quote }) await page.goto('/buy') // Wait for wallet to be connected diff --git a/apps/web/src/pages/Swap/Buy/hooks.ts b/apps/web/src/pages/Swap/Buy/hooks.ts index 975c5d00a40..41d10539e82 100644 --- a/apps/web/src/pages/Swap/Buy/hooks.ts +++ b/apps/web/src/pages/Swap/Buy/hooks.ts @@ -1,4 +1,5 @@ import { useMeldSupportedCurrencyToCurrencyInfo } from 'appGraphql/data/types' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useSearchParams } from 'react-router' @@ -17,8 +18,6 @@ import { FORCountry, OffRampTransferDetailsRequest, } from 'uniswap/src/features/fiatOnRamp/types' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' // biome-ignore lint/style/noRestrictedImports: Buy hooks need direct SDK imports import { getFiatCurrencyComponents } from 'utilities/src/format/localeBased' diff --git a/apps/web/src/pages/Swap/Fees.anvil.e2e.test.ts b/apps/web/src/pages/Swap/Fees.anvil.e2e.test.ts index cda699f73ca..9ba01796841 100644 --- a/apps/web/src/pages/Swap/Fees.anvil.e2e.test.ts +++ b/apps/web/src/pages/Swap/Fees.anvil.e2e.test.ts @@ -10,6 +10,7 @@ const test = getTest({ withAnvil: true }) test.describe('Fees', () => { test('swaps ETH for USDC exact-in with swap fee', async ({ page, anvil }) => { await stubTradingApiEndpoint({ page, endpoint: uniswapUrls.tradingApiPaths.swap }) + await stubTradingApiEndpoint({ page, endpoint: uniswapUrls.tradingApiPaths.quote }) await page.goto(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`) diff --git a/apps/web/src/pages/Swap/Fees.e2e.test.ts b/apps/web/src/pages/Swap/Fees.e2e.test.ts index 2c6564b3a16..c7b33190f5d 100644 --- a/apps/web/src/pages/Swap/Fees.e2e.test.ts +++ b/apps/web/src/pages/Swap/Fees.e2e.test.ts @@ -1,15 +1,16 @@ +import { Layers, PriceUxUpdateProperties } from '@universe/gating' import { expect, getTest } from 'playwright/fixtures' import { stubTradingApiEndpoint } from 'playwright/fixtures/tradingApi' import { Mocks } from 'playwright/mocks/mocks' import { DAI, USDC_MAINNET } from 'uniswap/src/constants/tokens' import { uniswapUrls } from 'uniswap/src/constants/urls' -import { Layers, PriceUxUpdateProperties } from 'uniswap/src/features/gating/experiments' import { TestID } from 'uniswap/src/test/fixtures/testIDs' const test = getTest() test.describe('Fees', () => { test('should not display fee on swaps without fees', async ({ page }) => { + await stubTradingApiEndpoint({ page, endpoint: uniswapUrls.tradingApiPaths.quote }) await page.goto(`/swap?inputCurrency=${DAI.address}&outputCurrency=${USDC_MAINNET.address}`) // Enter amount @@ -23,6 +24,7 @@ test.describe('Fees', () => { }) test('displays UniswapX fee in UI', async ({ page }) => { + await stubTradingApiEndpoint({ page, endpoint: uniswapUrls.tradingApiPaths.quote }) await stubTradingApiEndpoint({ page, endpoint: uniswapUrls.tradingApiPaths.swap }) await page.goto( diff --git a/apps/web/src/pages/Swap/Limit/LimitForm.tsx b/apps/web/src/pages/Swap/Limit/LimitForm.tsx index 6ae17550be7..762c656b716 100644 --- a/apps/web/src/pages/Swap/Limit/LimitForm.tsx +++ b/apps/web/src/pages/Swap/Limit/LimitForm.tsx @@ -1,5 +1,6 @@ import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core' import { UNIVERSAL_ROUTER_ADDRESS, UniversalRouterVersion } from '@uniswap/universal-router-sdk' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { useAccountDrawer } from 'components/AccountDrawer/MiniPortfolio/hooks' import { OpenLimitOrdersButton } from 'components/AccountDrawer/MiniPortfolio/Limits/OpenLimitOrdersButton' import { MenuStateVariant, useSetMenu } from 'components/AccountDrawer/menuState' @@ -40,8 +41,6 @@ import { uniswapUrls } from 'uniswap/src/constants/urls' import { LIMIT_SUPPORTED_CHAINS } from 'uniswap/src/features/chains/chainInfo' import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks/useSupportedChainId' import { getPrimaryStablecoin } from 'uniswap/src/features/chains/utils' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext' import { isEVMChain } from 'uniswap/src/features/platforms/utils/chains' import { useIsMismatchAccountQuery } from 'uniswap/src/features/smartWallet/mismatch/hooks' diff --git a/apps/web/src/pages/Swap/Logging.anvil.e2e.test.ts b/apps/web/src/pages/Swap/Logging.anvil.e2e.test.ts index 0c679d6bb32..ff6221750f6 100644 --- a/apps/web/src/pages/Swap/Logging.anvil.e2e.test.ts +++ b/apps/web/src/pages/Swap/Logging.anvil.e2e.test.ts @@ -16,6 +16,7 @@ test.describe('Time-to-swap logging', () => { anvil, }) => { await stubTradingApiEndpoint({ page, endpoint: uniswapUrls.tradingApiPaths.swap }) + await stubTradingApiEndpoint({ page, endpoint: uniswapUrls.tradingApiPaths.quote }) await page.goto(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`) const expectMultipleTransactions = createExpectMultipleTransactions({ diff --git a/apps/web/src/pages/Swap/Send/NewAddressSpeedBump.test.tsx b/apps/web/src/pages/Swap/Send/NewAddressSpeedBump.test.tsx index 7c2a01a89ad..35ef25f0b89 100644 --- a/apps/web/src/pages/Swap/Send/NewAddressSpeedBump.test.tsx +++ b/apps/web/src/pages/Swap/Send/NewAddressSpeedBump.test.tsx @@ -22,8 +22,9 @@ const mockSendContext: SendContextType = { setSendState: vi.fn(), } -vi.mock('uniswap/src/features/gating/hooks', () => { +vi.mock('@universe/gating', async (importOriginal) => { return { + ...(await importOriginal()), useFeatureFlag: vi.fn(), getFeatureFlag: vi.fn(), } diff --git a/apps/web/src/pages/Swap/Send/SendForm.tsx b/apps/web/src/pages/Swap/Send/SendForm.tsx index 064fc5844a0..69a680ca15e 100644 --- a/apps/web/src/pages/Swap/Send/SendForm.tsx +++ b/apps/web/src/pages/Swap/Send/SendForm.tsx @@ -13,7 +13,6 @@ import { useTranslation } from 'react-i18next' import { useSendContext } from 'state/send/SendContext' import { CurrencyState } from 'state/swap/types' import { Button, Flex } from 'ui/src' -import { checkIsBridgedAsset } from 'uniswap/src/components/BridgedAsset/utils' import { GetHelpHeader } from 'uniswap/src/components/dialog/GetHelpHeader' import { uniswapUrls } from 'uniswap/src/constants/urls' import { useActiveAddress, useConnectionStatus } from 'uniswap/src/features/accounts/store/hooks' @@ -114,7 +113,7 @@ function SendFormInner({ disableTokenInputs = false, onCurrencyChange }: SendFor const { tokenWarningDismissed: isCompatibleAddressDismissed } = useDismissedCompatibleAddressWarnings( inputCurrencyInfo?.currency, ) - const isUnichainBridgedAsset = checkIsBridgedAsset(inputCurrencyInfo ?? undefined) && !isCompatibleAddressDismissed + const isUnichainBridgedAsset = Boolean(inputCurrencyInfo?.isBridged) && !isCompatibleAddressDismissed const { isSmartContractAddress, loading: loadingSmartContractAddress } = useIsSmartContractAddress( recipientData?.address, diff --git a/apps/web/src/pages/Swap/Swap.anvil.e2e.test.ts b/apps/web/src/pages/Swap/Swap.anvil.e2e.test.ts index cc58e9a32fd..974ec8a1d6c 100644 --- a/apps/web/src/pages/Swap/Swap.anvil.e2e.test.ts +++ b/apps/web/src/pages/Swap/Swap.anvil.e2e.test.ts @@ -48,6 +48,7 @@ test.describe('Swap', () => { test('should swap ETH to USDC', async ({ page, anvil }) => { await stubTradingApiEndpoint({ page, endpoint: uniswapUrls.tradingApiPaths.swap }) + await stubTradingApiEndpoint({ page, endpoint: uniswapUrls.tradingApiPaths.quote }) await page.goto('/swap') await page.getByTestId(TestID.ChooseOutputToken).click() @@ -68,6 +69,8 @@ test.describe('Swap', () => { }) test('should be able to swap token with FOT warning via TDP', async ({ page, anvil }) => { + await stubTradingApiEndpoint({ page, endpoint: uniswapUrls.tradingApiPaths.quote }) + await page.route(`${uniswapUrls.tradingApiUrl}/v1/swap`, async (route) => { const request = route.request() const postData = request.postDataJSON() @@ -111,6 +114,8 @@ test.describe('Swap', () => { }) test('should bridge from ETH to L2', async ({ page, anvil }) => { + await stubTradingApiEndpoint({ page, endpoint: uniswapUrls.tradingApiPaths.swap }) + await stubTradingApiEndpoint({ page, endpoint: uniswapUrls.tradingApiPaths.quote }) await page.goto(`/swap?inputCurrency=ETH`) await page.getByTestId(TestID.ChooseOutputToken).click() await page.getByTestId(`token-option-${UniverseChainId.Base}-ETH`).first().click() @@ -122,6 +127,7 @@ test.describe('Swap', () => { ).toBeVisible() await page.getByTestId(TestID.AmountInputIn).click() await page.getByTestId(TestID.AmountInputIn).fill('1') + await expect(page.getByTestId(TestID.ReviewSwap)).toBeEnabled() await page.getByTestId(TestID.ReviewSwap).click() await page.getByTestId(TestID.Confirm).click() await page.getByTestId(TestID.Swap).click() @@ -198,7 +204,7 @@ test.describe('Swap', () => { await page.getByTestId(TestID.ReviewSwap).click() await page.getByTestId(TestID.Swap).click() - await expect(page.getByText('Sign Message')).not.toBeVisible() + await expect(page.getByText('Sign message')).not.toBeVisible() await expect(page.getByText('Approved')).toBeVisible() await expect(page.getByText('Swapped')).toBeVisible() }) @@ -239,48 +245,48 @@ test.describe('Swap', () => { await page.getByTestId(TestID.Swap).click() await expect(page.getByText('Reset USDT limit')).toBeVisible() - await expect(page.getByText('Sign Message')).toBeVisible() + await expect(page.getByText('Sign message')).toBeVisible() await expect(page.getByText('Approved')).toBeVisible() await expect(page.getByText('Swapped')).toBeVisible() }) test('prompts signature when existing permit approval is expired', async ({ page, anvil }) => { + await stubTradingApiEndpoint({ page, endpoint: uniswapUrls.tradingApiPaths.swap }) + await anvil.setErc20Balance({ address: assume0xAddress(USDT.address), balance: ONE_MILLION_USDT }) await anvil.setPermit2Allowance({ owner: TEST_WALLET_ADDRESS, token: assume0xAddress(USDT.address), spender: assume0xAddress(UNIVERSAL_ROUTER_ADDRESS(UniversalRouterVersion.V2_0, UniverseChainId.Mainnet)), expiration: Math.floor((Date.now() - 1) / 1000), }) - await stubTradingApiEndpoint({ page, endpoint: uniswapUrls.tradingApiPaths.swap }) - await anvil.setErc20Balance({ address: assume0xAddress(USDT.address), balance: ONE_MILLION_USDT }) await page.goto(`/swap?inputCurrency=${USDT.address}&outputCurrency=ETH`) await page.getByTestId(TestID.AmountInputIn).click() await page.getByTestId(TestID.AmountInputIn).fill('10') await page.getByTestId(TestID.ReviewSwap).click() await page.getByTestId(TestID.Swap).click() + await expect(page.getByText('Sign message')).toBeVisible() await expect(page.getByText('Approved')).toBeVisible() - await expect(page.getByText('Sign Message')).toBeVisible() await expect(page.getByText('Swapped')).toBeVisible() }) test('prompts signature when existing permit approval amount is too low', async ({ page, anvil }) => { + await stubTradingApiEndpoint({ page, endpoint: uniswapUrls.tradingApiPaths.swap }) + await anvil.setErc20Balance({ address: assume0xAddress(USDT.address), balance: ONE_MILLION_USDT }) await anvil.setPermit2Allowance({ owner: TEST_WALLET_ADDRESS, token: assume0xAddress(USDT.address), spender: assume0xAddress(UNIVERSAL_ROUTER_ADDRESS(UniversalRouterVersion.V2_0, UniverseChainId.Mainnet)), amount: 1n, }) - await stubTradingApiEndpoint({ page, endpoint: uniswapUrls.tradingApiPaths.swap }) - await anvil.setErc20Balance({ address: assume0xAddress(USDT.address), balance: ONE_MILLION_USDT }) await page.goto(`/swap?inputCurrency=${USDT.address}&outputCurrency=ETH`) await page.getByTestId(TestID.AmountInputIn).click() await page.getByTestId(TestID.AmountInputIn).fill('10') await page.getByTestId(TestID.ReviewSwap).click() await page.getByTestId(TestID.Swap).click() + await expect(page.getByText('Sign message')).toBeVisible() await expect(page.getByText('Approved')).toBeVisible() - await expect(page.getByText('Sign Message')).toBeVisible() await expect(page.getByText('Swapped')).toBeVisible() }) }) diff --git a/apps/web/src/pages/Swap/index.tsx b/apps/web/src/pages/Swap/index.tsx index c17bb5ba55a..a06928cb810 100644 --- a/apps/web/src/pages/Swap/index.tsx +++ b/apps/web/src/pages/Swap/index.tsx @@ -1,5 +1,6 @@ import { PrefetchBalancesWrapper } from 'appGraphql/data/apollo/AdaptiveTokenBalancesProvider' import type { Currency } from '@uniswap/sdk-core' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { useAccountDrawer } from 'components/AccountDrawer/MiniPortfolio/hooks' import { SwapBottomCard } from 'components/SwapBottomCard' import { SwitchLocaleLink } from 'components/SwitchLocaleLink' @@ -28,10 +29,8 @@ import type { AppTFunction } from 'ui/src/i18n/types' import { zIndexes } from 'ui/src/theme' import { useUniswapContext } from 'uniswap/src/contexts/UniswapContext' import { useIsModeMismatch } from 'uniswap/src/features/chains/hooks/useEnabledChains' -import type { UniverseChainId } from 'uniswap/src/features/chains/types' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { RampDirection } from 'uniswap/src/features/fiatOnRamp/types' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { useGetPasskeyAuthStatus } from 'uniswap/src/features/passkey/hooks/useGetPasskeyAuthStatus' import { WebFORNudgeProvider } from 'uniswap/src/features/providers/webForNudgeProvider' import { InterfaceEventName, InterfacePageName, ModalName } from 'uniswap/src/features/telemetry/constants' @@ -49,6 +48,7 @@ import { SwapDependenciesStoreContextProvider } from 'uniswap/src/features/trans import { SwapFormStoreContextProvider } from 'uniswap/src/features/transactions/swap/stores/swapFormStore/SwapFormStoreContextProvider' import type { SwapFormState } from 'uniswap/src/features/transactions/swap/stores/swapFormStore/types' import { currencyToAsset } from 'uniswap/src/features/transactions/swap/utils/asset' +import { TransactionState } from 'uniswap/src/features/transactions/types/transactionState' import { CurrencyField } from 'uniswap/src/types/currency' import { SwapTab } from 'uniswap/src/types/screens/interface' import { isMobileWeb } from 'utilities/src/platform' @@ -66,7 +66,8 @@ export default function SwapPage() { const { initialInputCurrency, initialOutputCurrency, - initialChainId, + initialInputChainId, + initialOutputChainId, initialTypedValue, initialField, triggerConnect, @@ -84,9 +85,10 @@ export default function SwapPage() { void initialInputCurrency?: Currency initialOutputCurrency?: Currency + initialOutputChainId?: UniverseChainId initialTypedValue?: string initialIndependentField?: CurrencyField syncTabToUrl: boolean @@ -137,7 +158,7 @@ export function Swap({ const { isSwapTokenSelectorOpen, swapOutputChainId } = useUniswapContext() const isExplorePage = useIsPage(PageType.EXPLORE) - const isModeMismatch = useIsModeMismatch(chainId) + const isModeMismatch = useIsModeMismatch(initialInputChainId) const isSharedSwapDisabled = isModeMismatch && isExplorePage const input = currencyToAsset(initialInputCurrency) @@ -153,11 +174,16 @@ export function Swap({ selectingCurrencyField: isSwapTokenSelectorOpen ? CurrencyField.OUTPUT : undefined, selectingCurrencyChainId: swapOutputChainId, skipFocusOnCurrencyField: isMobileWeb, - filteredChainIdsOverride: usePersistedFilteredChainIds ? persistedFilteredChainIds : undefined, + filteredChainIdsOverride: getFilteredChainIdsOverride({ + initialInputChainId, + initialOutputChainId, + usePersistedFilteredChainIds, + persistedFilteredChainIds, + }), }) return ( - + ): AnvilConfig { port: overrides?.port ?? parseInt(process.env.ANVIL_PORT ?? '8545'), host: overrides?.host ?? '127.0.0.1', forkUrl: overrides?.forkUrl ?? buildForkUrl(), - timeout: overrides?.timeout ?? 5000, - healthCheckInterval: overrides?.healthCheckInterval ?? 10000, + timeout: overrides?.timeout ?? 10_000, + healthCheckInterval: overrides?.healthCheckInterval ?? 10_000, logFile: overrides?.logFile ?? path.join(process.cwd(), `anvil-test-${process.pid}.log`), } } diff --git a/apps/web/src/playwright/fixtures/anvil.ts b/apps/web/src/playwright/fixtures/anvil.ts index d80cd8c8bc4..91fcab8dffb 100644 --- a/apps/web/src/playwright/fixtures/anvil.ts +++ b/apps/web/src/playwright/fixtures/anvil.ts @@ -10,7 +10,9 @@ import { ZERO_ADDRESS } from 'uniswap/src/constants/misc' import { DAI, USDT } from 'uniswap/src/constants/tokens' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { assume0xAddress } from 'utils/wagmi' -import { Address, erc20Abi } from 'viem' +import { type Address, erc20Abi } from 'viem' + +const SNAPSHOTS_ENABLED = process.env.ENABLE_ANVIL_SNAPSHOTS === 'true' class WalletError extends Error { code?: number @@ -162,6 +164,11 @@ const createAnvilClient = () => { await client.mine({ blocks: 1 }) }, + /** + * @deprecated + * Wagmi submits transactions to Anvil via the RPC interface so this function no longer intercepts + * the requests. Use createRejectableMockConnector instead. + */ async setTransactionRejection() { // Override the wallet actions to reject transactions const originalRequest = client.request @@ -219,7 +226,9 @@ export const test = base.extend<{ anvil: AnvilClient; delegateToZeroAddress?: vo // Take snapshot for test isolation let snapshotId: `0x${string}` | undefined try { - snapshotId = await testAnvil.snapshot() + if (SNAPSHOTS_ENABLED) { + snapshotId = await testAnvil.snapshot() + } } catch (error) { if (isTimeoutError(error)) { // Anvil timed out during snapshot, restart and retry diff --git a/apps/web/src/playwright/fixtures/tradingApi.ts b/apps/web/src/playwright/fixtures/tradingApi.ts index b84f79de3b1..0092111e8b4 100644 --- a/apps/web/src/playwright/fixtures/tradingApi.ts +++ b/apps/web/src/playwright/fixtures/tradingApi.ts @@ -1,6 +1,6 @@ // biome-ignore lint/style/noRestrictedImports: Trading API fixtures need direct Playwright imports import { test as base } from '@playwright/test' -import { Page } from 'playwright/test' +import { type Page } from 'playwright/test' import { uniswapUrls } from 'uniswap/src/constants/urls' export const DEFAULT_TEST_GAS_LIMIT = '20000000' @@ -54,7 +54,15 @@ export async function stubTradingApiEndpoint({ }) const responseText = await response.text() - let responseJson = JSON.parse(responseText) + let responseJson + try { + responseJson = JSON.parse(responseText) + } catch (parseError) { + throw new Error(`Failed to parse trading API response for ${endpoint}. Response: ${responseText}`, { + cause: parseError, + }) + } + // Set a high gas limit to avoid OutOfGas if (endpoint === uniswapUrls.tradingApiPaths.swap) { responseJson.swap.gasLimit = DEFAULT_TEST_GAS_LIMIT diff --git a/apps/web/src/setupTests.ts b/apps/web/src/setupTests.ts index 9ede1b601e6..27620e661a8 100644 --- a/apps/web/src/setupTests.ts +++ b/apps/web/src/setupTests.ts @@ -10,6 +10,7 @@ import { WalletName, WalletReadyState, } from '@solana/wallet-adapter-base' +import { useFeatureFlag } from '@universe/gating' import { useWeb3React } from '@web3-react/core' import { config as loadEnv } from 'dotenv' import failOnConsole from 'jest-fail-on-console' @@ -19,7 +20,6 @@ import { Readable } from 'stream' import { toBeVisible } from 'test-utils/matchers' import { mocked } from 'test-utils/mocked' import { UniverseChainId } from 'uniswap/src/features/chains/types' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { setupi18n } from 'uniswap/src/i18n/i18n-setup-interface' import { mockLocalizationContext } from 'uniswap/src/test/mocks/locale' import { TextDecoder, TextEncoder } from 'util' @@ -390,10 +390,9 @@ failOnConsole({ }, }) -vi.mock('uniswap/src/features/gating/hooks', async () => { - const genMock = await vi.importActual('uniswap/src/features/gating/hooks') +vi.mock('@universe/gating', async (importOriginal) => { return { - ...genMock, + ...(await importOriginal()), useFeatureFlag: vi.fn(), useFeatureFlagWithLoading: vi.fn(), getFeatureFlag: vi.fn(), diff --git a/apps/web/src/state/activity/polling/transactions.ts b/apps/web/src/state/activity/polling/transactions.ts index 4bbbb3fae73..417397b7707 100644 --- a/apps/web/src/state/activity/polling/transactions.ts +++ b/apps/web/src/state/activity/polling/transactions.ts @@ -1,4 +1,5 @@ import { TradingApi } from '@universe/api' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { useAccount } from 'hooks/useAccount' import useCurrentBlockTimestamp from 'hooks/useCurrentBlockTimestamp' import useBlockNumber from 'lib/hooks/useBlockNumber' @@ -13,8 +14,6 @@ import { isPendingTx } from 'state/transactions/utils' import { TradingApiClient } from 'uniswap/src/data/apiClients/tradingApi/TradingApiClient' import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' import { RetryOptions, UniverseChainId } from 'uniswap/src/features/chains/types' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { InterfaceEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { checkedTransaction } from 'uniswap/src/features/transactions/slice' diff --git a/apps/web/src/state/explore/protocolStats.test.tsx b/apps/web/src/state/explore/protocolStats.test.tsx index 0c94c9521fb..55ee77f344a 100644 --- a/apps/web/src/state/explore/protocolStats.test.tsx +++ b/apps/web/src/state/explore/protocolStats.test.tsx @@ -1,14 +1,13 @@ import { ProtocolStatsResponse } from '@uniswap/client-explore/dist/uniswap/explore/v1/service_pb' +import { useFeatureFlagWithLoading } from '@universe/gating' import { ExploreContext } from 'state/explore' import { use24hProtocolVolume, useDailyTVLWithChange } from 'state/explore/protocolStats' import { render, screen } from 'test-utils/render' -import * as GatingHooks from 'uniswap/src/features/gating/hooks' import type { Mock } from 'vitest' -vi.mock('uniswap/src/features/gating/hooks', async () => { - const actual = await vi.importActual('uniswap/src/features/gating/hooks') +vi.mock('@universe/gating', async (importOriginal) => { return { - ...actual, + ...(await importOriginal()), useFeatureFlagWithLoading: vi.fn(() => ({ value: true, isLoading: false })), // Ensure mock returns value immediately } }) @@ -57,7 +56,7 @@ const TestComponent24HrTVL = () => { } beforeEach(() => { - ;(GatingHooks.useFeatureFlagWithLoading as Mock).mockReturnValue({ value: true, isLoading: false }) + ;(useFeatureFlagWithLoading as Mock).mockReturnValue({ value: true, isLoading: false }) }) describe('use24hProtocolVolume', () => { diff --git a/apps/web/src/state/explore/topPools.ts b/apps/web/src/state/explore/topPools.ts index b8f97566868..980f69ad586 100644 --- a/apps/web/src/state/explore/topPools.ts +++ b/apps/web/src/state/explore/topPools.ts @@ -5,8 +5,8 @@ import { PoolTableSortState, } from 'appGraphql/data/pools/useTopPools' import { OrderDirection } from 'appGraphql/data/util' +import { ProtocolVersion } from '@uniswap/client-data-api/dist/data/v1/poolTypes_pb' import { ExploreStatsResponse, PoolStats } from '@uniswap/client-explore/dist/uniswap/explore/v1/service_pb' -import { ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' import { exploreSearchStringAtom } from 'components/Tokens/state' import { useAtomValue } from 'jotai/utils' import { useContext, useMemo } from 'react' diff --git a/apps/web/src/state/limit/hooks.ts b/apps/web/src/state/limit/hooks.ts index d63821e61f4..5e04da2d683 100644 --- a/apps/web/src/state/limit/hooks.ts +++ b/apps/web/src/state/limit/hooks.ts @@ -1,4 +1,5 @@ import { Currency, CurrencyAmount, Price, TradeType } from '@uniswap/sdk-core' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { useAccount } from 'hooks/useAccount' import JSBI from 'jsbi' import { useCurrencyBalances } from 'lib/hooks/useCurrencyBalance' @@ -13,8 +14,6 @@ import { getUSDCostPerGas, isClassicTrade } from 'state/routing/utils' import { useSwapAndLimitContext } from 'state/swap/useSwapContext' import { nativeOnChain } from 'uniswap/src/constants/tokens' import { getStablecoinsForChain, isUniverseChainId } from 'uniswap/src/features/chains/utils' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { isEVMChain, isSVMChain } from 'uniswap/src/features/platforms/utils/chains' import { CurrencyField } from 'uniswap/src/types/currency' diff --git a/apps/web/src/state/routing/useRoutingAPITrade.test.ts b/apps/web/src/state/routing/useRoutingAPITrade.test.ts index f535380851d..208225f4531 100644 --- a/apps/web/src/state/routing/useRoutingAPITrade.test.ts +++ b/apps/web/src/state/routing/useRoutingAPITrade.test.ts @@ -24,8 +24,9 @@ vi.mock('./slice', () => { } }) vi.mock('state/user/hooks') -vi.mock('uniswap/src/features/gating/hooks', () => { +vi.mock('@universe/gating', async (importOriginal) => { return { + ...(await importOriginal()), useFeatureFlag: vi.fn(), useExperimentValue: vi.fn(), getFeatureFlag: vi.fn(), diff --git a/apps/web/src/state/sagas/liquidity/liquiditySaga.ts b/apps/web/src/state/sagas/liquidity/liquiditySaga.ts index 492091bf334..1f96ff38e72 100644 --- a/apps/web/src/state/sagas/liquidity/liquiditySaga.ts +++ b/apps/web/src/state/sagas/liquidity/liquiditySaga.ts @@ -5,7 +5,6 @@ import { import { getLiquidityEventName } from 'components/Liquidity/analytics' import { popupRegistry } from 'components/Popups/registry' import { PopupType } from 'components/Popups/types' -import type { HandleOnChainStepParams } from 'state/sagas/transactions/utils' import { getDisplayableError, handleApprovalTransactionStep, @@ -34,7 +33,7 @@ import type { } from 'uniswap/src/features/transactions/liquidity/steps/migrate' import type { LiquidityAction, ValidatedLiquidityTxContext } from 'uniswap/src/features/transactions/liquidity/types' import { LiquidityTransactionType } from 'uniswap/src/features/transactions/liquidity/types' -import type { TransactionStep } from 'uniswap/src/features/transactions/steps/types' +import type { HandleOnChainStepParams, TransactionStep } from 'uniswap/src/features/transactions/steps/types' import { TransactionStepType } from 'uniswap/src/features/transactions/steps/types' import type { SetCurrentStepFn } from 'uniswap/src/features/transactions/swap/types/swapCallback' import type { diff --git a/apps/web/src/state/sagas/root.ts b/apps/web/src/state/sagas/root.ts index 7ebf0f49ac1..88b64fae9a5 100644 --- a/apps/web/src/state/sagas/root.ts +++ b/apps/web/src/state/sagas/root.ts @@ -4,10 +4,12 @@ import { swapSaga } from 'state/sagas/transactions/swapSaga' import { watchTransactionsSaga } from 'state/sagas/transactions/watcherSaga' import { wrapSaga } from 'state/sagas/transactions/wrapSaga' import { call, spawn } from 'typed-redux-saga' +import { planSaga } from 'uniswap/src/features/transactions/swap/plan/planSaga' import { waitForRehydration } from 'uniswap/src/utils/saga' const sagas = [ swapSaga.wrappedSaga, + planSaga.wrappedSaga, wrapSaga.wrappedSaga, liquiditySaga.wrappedSaga, watchTransactionsSaga.wrappedSaga, diff --git a/apps/web/src/state/sagas/transactions/5792.ts b/apps/web/src/state/sagas/transactions/5792.ts index 30f39529ce0..f10eed438aa 100644 --- a/apps/web/src/state/sagas/transactions/5792.ts +++ b/apps/web/src/state/sagas/transactions/5792.ts @@ -4,12 +4,12 @@ import { popupRegistry } from 'components/Popups/registry' import { PopupType } from 'components/Popups/types' import { wagmiConfig } from 'components/Web3Provider/wagmiConfig' import { getRoutingForTransaction } from 'state/activity/utils' -import { getSigner, HandleOnChainStepParams, watchForInterruption } from 'state/sagas/transactions/utils' +import { getSigner, watchForInterruption } from 'state/sagas/transactions/utils' import { handleGetCapabilities } from 'state/walletCapabilities/lib/handleGetCapabilities' import { setCapabilitiesByChain } from 'state/walletCapabilities/reducer' import { call, put } from 'typed-redux-saga' import { addTransaction } from 'uniswap/src/features/transactions/slice' -import { OnChainTransactionStepBatched } from 'uniswap/src/features/transactions/steps/types' +import { HandleOnChainStepParams, OnChainTransactionStepBatched } from 'uniswap/src/features/transactions/steps/types' import { InterfaceTransactionDetails, TransactionOriginType, diff --git a/apps/web/src/state/sagas/transactions/solana.ts b/apps/web/src/state/sagas/transactions/solana.ts index 7451e96e51a..2ff23ceb5ac 100644 --- a/apps/web/src/state/sagas/transactions/solana.ts +++ b/apps/web/src/state/sagas/transactions/solana.ts @@ -5,9 +5,10 @@ import { PopupType } from 'components/Popups/types' import { signSolanaTransactionWithCurrentWallet } from 'components/Web3Provider/signSolanaTransaction' import store from 'state' import { getSwapTransactionInfo } from 'state/sagas/transactions/utils' -import { call } from 'typed-redux-saga' +import { call, delay, spawn } from 'typed-redux-saga' import { JupiterApiClient } from 'uniswap/src/data/apiClients/jupiterApi/JupiterFetchClient' import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { refetchRestQueriesViaOnchainOverrideVariant } from 'uniswap/src/features/portfolio/portfolioUpdates/rest/refetchRestQueriesViaOnchainOverrideVariantSaga' import { SwapEventName } from 'uniswap/src/features/telemetry/constants/features' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { JupiterExecuteError } from 'uniswap/src/features/transactions/errors' @@ -16,9 +17,15 @@ import { ExtractedBaseTradeAnalyticsProperties } from 'uniswap/src/features/tran import { SolanaTrade } from 'uniswap/src/features/transactions/swap/types/solana' import { ValidatedSolanaSwapTxAndGasInfo } from 'uniswap/src/features/transactions/swap/types/swapTxAndGasInfo' import { SwapEventType, timestampTracker } from 'uniswap/src/features/transactions/swap/utils/SwapEventTimestampTracker' -import { TransactionOriginType, TransactionStatus } from 'uniswap/src/features/transactions/types/transactionDetails' +import { + InterfaceBaseTransactionDetails, + SolanaTransactionDetails, + TransactionOriginType, + TransactionStatus, +} from 'uniswap/src/features/transactions/types/transactionDetails' import { SignerMnemonicAccountDetails } from 'uniswap/src/features/wallet/types/AccountDetails' import { tryCatch } from 'utilities/src/errors' +import { ONE_SECOND_MS } from 'utilities/src/time/time' type JupiterSwapParams = { account: SignerMnemonicAccountDetails @@ -49,27 +56,52 @@ async function signAndSendJupiterSwap({ return result } -function updateAppState({ hash, trade, from }: { hash: string; trade: SolanaTrade; from: string }) { +function* refetchBalancesWithDelay({ + transaction, + activeAddress, +}: { + transaction: SolanaTransactionDetails + activeAddress: string +}) { + // Wait 3 seconds before refetching. + // This is because at this point the transaction hasn't been fully confirmed yet, + // and it should take 1-2 seconds for the balance to update onchain. + yield* delay(3 * ONE_SECOND_MS) + + yield* call(refetchRestQueriesViaOnchainOverrideVariant, { + transaction, + activeAddress, + apolloClient: null, + }) +} + +function* updateAppState({ hash, trade, from }: { hash: string; trade: SolanaTrade; from: string }) { const typeInfo = getSwapTransactionInfo(trade) - store.dispatch( - addTransaction({ - from, - typeInfo, - hash, - chainId: UniverseChainId.Solana, - routing: TradingApi.Routing.JUPITER, - status: TransactionStatus.Success, - addedTime: Date.now(), - id: hash, - transactionOriginType: TransactionOriginType.Internal, - options: { - request: {}, - }, - }), - ) + const transaction: SolanaTransactionDetails = { + from, + typeInfo, + hash, + chainId: UniverseChainId.Solana, + routing: TradingApi.Routing.JUPITER, + status: TransactionStatus.Success, + addedTime: Date.now(), + id: hash, + transactionOriginType: TransactionOriginType.Internal, + options: { + request: {}, + }, + } + + store.dispatch(addTransaction(transaction)) popupRegistry.addPopup({ type: PopupType.Transaction, hash }, hash) + + // Spawn background task to refetch balances after a delay + yield* spawn(refetchBalancesWithDelay, { + transaction, + activeAddress: from, + }) } function createJupiterSwap(signSolanaTransaction: (tx: VersionedTransaction) => Promise) { @@ -92,7 +124,7 @@ function createJupiterSwap(signSolanaTransaction: (tx: VersionedTransaction) => throw new JupiterExecuteError(errorMessage ?? 'Unknown Jupiter Execution Error', code) } - updateAppState({ hash, trade, from: account.address }) + yield* call(updateAppState, { hash, trade, from: account.address }) return hash } diff --git a/apps/web/src/state/sagas/transactions/swapSaga.ts b/apps/web/src/state/sagas/transactions/swapSaga.ts index 2187773687e..f24c0bcfe3c 100644 --- a/apps/web/src/state/sagas/transactions/swapSaga.ts +++ b/apps/web/src/state/sagas/transactions/swapSaga.ts @@ -1,5 +1,6 @@ import { useTotalBalancesUsdForAnalytics } from 'appGraphql/data/apollo/useTotalBalancesUsdForAnalytics' import { TradingApi } from '@universe/api' +import { Experiments } from '@universe/gating' import { popupRegistry } from 'components/Popups/registry' import { PopupType } from 'components/Popups/types' import { DEFAULT_TXN_DISMISS_MS, L2_TXN_DISMISS_MS, ZERO_PERCENT } from 'constants/misc' @@ -16,7 +17,6 @@ import { handleUniswapXSignatureStep } from 'state/sagas/transactions/uniswapx' import { getDisplayableError, getSwapTransactionInfo, - HandleOnChainStepParams, handleApprovalTransactionStep, handleOnChainStep, handlePermitTransactionStep, @@ -24,24 +24,31 @@ import { } from 'state/sagas/transactions/utils' import { VitalTxFields } from 'state/transactions/types' import invariant from 'tiny-invariant' -import { call } from 'typed-redux-saga' +import { call, SagaGenerator } from 'typed-redux-saga' import { isL2ChainId } from 'uniswap/src/features/chains/utils' import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext' import { isSVMChain } from 'uniswap/src/features/platforms/utils/chains' import { SwapEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { SwapTradeBaseProperties } from 'uniswap/src/features/telemetry/types' +import { logExperimentQualifyingEvent } from 'uniswap/src/features/telemetry/utils/logExperimentQualifyingEvent' import { selectSwapStartTimestamp } from 'uniswap/src/features/timing/selectors' import { updateSwapStartTimestamp } from 'uniswap/src/features/timing/slice' import { UnexpectedTransactionStateError } from 'uniswap/src/features/transactions/errors' -import { TransactionStep, TransactionStepType } from 'uniswap/src/features/transactions/steps/types' +import { + HandleOnChainStepParams, + HandleSwapStepParams, + TransactionStep, + TransactionStepType, +} from 'uniswap/src/features/transactions/steps/types' import { ExtractedBaseTradeAnalyticsProperties, getBaseTradeAnalyticsProperties, } from 'uniswap/src/features/transactions/swap/analytics' -import { FLASHBLOCKS_UI_SKIP_ROUTES } from 'uniswap/src/features/transactions/swap/components/UnichainInstantBalanceModal/constants' -import { getIsFlashblocksEnabled } from 'uniswap/src/features/transactions/swap/hooks/useIsUnichainFlashblocksEnabled' +import { getFlashblocksExperimentStatus } from 'uniswap/src/features/transactions/swap/hooks/useIsUnichainFlashblocksEnabled' import { useV4SwapEnabled } from 'uniswap/src/features/transactions/swap/hooks/useV4SwapEnabled' +import { planSaga } from 'uniswap/src/features/transactions/swap/plan/planSaga' +import { handleSwitchChains } from 'uniswap/src/features/transactions/swap/plan/utils' import { SwapTransactionStep, SwapTransactionStepAsync, @@ -54,10 +61,15 @@ import { SwapCallbackParams, } from 'uniswap/src/features/transactions/swap/types/swapCallback' import { PermitMethod, ValidatedSwapTxContext } from 'uniswap/src/features/transactions/swap/types/swapTxAndGasInfo' -import { BridgeTrade, ClassicTrade } from 'uniswap/src/features/transactions/swap/types/trade' +import { BridgeTrade, ChainedActionTrade, ClassicTrade } from 'uniswap/src/features/transactions/swap/types/trade' import { slippageToleranceToPercent } from 'uniswap/src/features/transactions/swap/utils/format' import { generateSwapTransactionSteps } from 'uniswap/src/features/transactions/swap/utils/generateSwapTransactionSteps' -import { isClassic, isJupiter, UNISWAPX_ROUTING_VARIANTS } from 'uniswap/src/features/transactions/swap/utils/routing' +import { + isClassic, + isJupiter, + requireRouting, + UNISWAPX_ROUTING_VARIANTS, +} from 'uniswap/src/features/transactions/swap/utils/routing' import { getClassicQuoteFromResponse } from 'uniswap/src/features/transactions/swap/utils/tradingApi' import { useWallet } from 'uniswap/src/features/wallet/hooks/useWallet' import { @@ -68,14 +80,7 @@ import { createSaga } from 'uniswap/src/utils/saga' import { logger } from 'utilities/src/logger/logger' import { useTrace } from 'utilities/src/telemetry/trace/TraceContext' -interface HandleSwapStepParams extends Omit { - step: SwapTransactionStep | SwapTransactionStepAsync - signature?: string - trade: ClassicTrade | BridgeTrade - analytics: ExtractedBaseTradeAnalyticsProperties - onTransactionHash?: (hash: string) => void -} -function* handleSwapTransactionStep(params: HandleSwapStepParams) { +function* handleSwapTransactionStep(params: HandleSwapStepParams): SagaGenerator { const { trade, step, signature, analytics, onTransactionHash } = params const info = getSwapTransactionInfo(trade) @@ -103,14 +108,24 @@ function* handleSwapTransactionStep(params: HandleSwapStepParams) { handleSwapTransactionAnalytics({ ...params, hash }) - if ( - !getIsFlashblocksEnabled(trade.inputAmount.currency.chainId) || - FLASHBLOCKS_UI_SKIP_ROUTES.includes(trade.routing) - ) { + const chainId = trade.inputAmount.currency.chainId + const { shouldLogQualifyingEvent, shouldShowModal } = getFlashblocksExperimentStatus({ + chainId, + routing: trade.routing, + }) + + if (shouldLogQualifyingEvent) { + logExperimentQualifyingEvent({ + experiment: Experiments.UnichainFlashblocksModal, + }) + } + + // Show regular popup for control variant or ineligible swaps + if (!shouldShowModal) { popupRegistry.addPopup( { type: PopupType.Transaction, hash }, hash, - isL2ChainId(trade.inputAmount.currency.chainId) ? L2_TXN_DISMISS_MS : DEFAULT_TXN_DISMISS_MS, + isL2ChainId(chainId) ? L2_TXN_DISMISS_MS : DEFAULT_TXN_DISMISS_MS, ) } @@ -119,7 +134,7 @@ function* handleSwapTransactionStep(params: HandleSwapStepParams) { onTransactionHash(hash) } - return + return hash } interface HandleSwapBatchedStepParams extends Omit { @@ -149,7 +164,7 @@ function* handleSwapTransactionBatchedStep(params: HandleSwapBatchedStepParams) } function handleSwapTransactionAnalytics(params: { - trade: ClassicTrade | BridgeTrade + trade: ClassicTrade | BridgeTrade | ChainedActionTrade analytics: SwapTradeBaseProperties hash?: string batchId?: string @@ -208,31 +223,6 @@ type SwapParams = { v4Enabled: boolean } -/** Asserts that a given object fits a given routing variant. */ -function requireRouting( - val: V, - routing: readonly T[], -): asserts val is V & { routing: T } { - if (!routing.includes(val.routing as T)) { - throw new UnexpectedTransactionStateError(`Expected routing ${routing}, got ${val.routing}`) - } -} - -/** Switches to the proper chain, if needed. If a chain switch is necessary and it fails, returns success=false. */ -async function handleSwitchChains( - params: Pick, -): Promise<{ chainSwitchFailed: boolean }> { - const { selectChain, startChainId, swapTxContext } = params - - const swapChainId = swapTxContext.trade.inputAmount.currency.chainId - if (isJupiter(swapTxContext) || swapChainId === startChainId) { - return { chainSwitchFailed: false } - } - - const chainSwitched = await selectChain(swapChainId) - return { chainSwitchFailed: !chainSwitched } -} - function* swap(params: SwapParams) { const { account, @@ -247,7 +237,11 @@ function* swap(params: SwapParams) { } = params const { trade } = swapTxContext - const { chainSwitchFailed } = yield* call(handleSwitchChains, params) + const { chainSwitchFailed } = yield* call(handleSwitchChains, { + selectChain: params.selectChain, + startChainId: params.startChainId, + swapTxContext, + }) if (chainSwitchFailed) { onFailure() return @@ -408,7 +402,19 @@ export function useSwapCallback(): SwapCallback { updateSwapForm({ txHash: hash, txHashReceivedTime: Date.now() }) }, } - appDispatch(swapSaga.actions.trigger(swapParams)) + if (swapTxContext.trade.routing === TradingApi.Routing.CHAINED) { + appDispatch( + planSaga.actions.trigger({ + ...swapParams, + handleApprovalTransactionStep, + handleSwapTransactionStep, + handleSignatureStep, + getDisplayableError, + }), + ) + } else { + appDispatch(swapSaga.actions.trigger(swapParams)) + } const blockNumber = getClassicQuoteFromResponse(trade.quote)?.blockNumber?.toString() diff --git a/apps/web/src/state/sagas/transactions/uniswapx.ts b/apps/web/src/state/sagas/transactions/uniswapx.ts index fee5167d8bf..fe118260a6a 100644 --- a/apps/web/src/state/sagas/transactions/uniswapx.ts +++ b/apps/web/src/state/sagas/transactions/uniswapx.ts @@ -4,7 +4,6 @@ import { formatSwapSignedAnalyticsEventProperties } from 'lib/utils/analytics' import { addTransactionBreadcrumb, getSwapTransactionInfo, - HandleSignatureStepParams, handleSignatureStep, TransactionBreadcrumbStatus, } from 'state/sagas/transactions/utils' @@ -15,6 +14,7 @@ import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { SwapTradeBaseProperties } from 'uniswap/src/features/telemetry/types' import { HandledTransactionInterrupt } from 'uniswap/src/features/transactions/errors' import { addTransaction } from 'uniswap/src/features/transactions/slice' +import { HandleSignatureStepParams } from 'uniswap/src/features/transactions/steps/types' import { UniswapXSignatureStep } from 'uniswap/src/features/transactions/swap/steps/signOrder' import { UniswapXTrade } from 'uniswap/src/features/transactions/swap/types/trade' import { slippageToleranceToPercent } from 'uniswap/src/features/transactions/swap/utils/format' diff --git a/apps/web/src/state/sagas/transactions/utils.ts b/apps/web/src/state/sagas/transactions/utils.ts index bd12ae770d0..1905efecf5a 100644 --- a/apps/web/src/state/sagas/transactions/utils.ts +++ b/apps/web/src/state/sagas/transactions/utils.ts @@ -3,6 +3,7 @@ import type { TransactionResponse } from '@ethersproject/abstract-provider' import type { JsonRpcSigner, Web3Provider } from '@ethersproject/providers' import { TradeType } from '@uniswap/sdk-core' import { FetchError, TradingApi } from '@universe/api' +import { BlockedAsyncSubmissionChainIdsConfigKey, DynamicConfigs, getDynamicConfigValue } from '@universe/gating' import { wagmiConfig } from 'components/Web3Provider/wagmiConfig' import { clientToProvider } from 'hooks/useEthersProvider' import ms from 'ms' @@ -15,8 +16,6 @@ import type { SagaGenerator } from 'typed-redux-saga' import { call, cancel, delay, fork, put, race, select, spawn, take } from 'typed-redux-saga' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { isL2ChainId, isUniverseChainId } from 'uniswap/src/features/chains/utils' -import { BlockedAsyncSubmissionChainIdsConfigKey, DynamicConfigs } from 'uniswap/src/features/gating/configs' -import { getDynamicConfigValue } from 'uniswap/src/features/gating/hooks' import { ApprovalEditedInWalletError, HandledTransactionInterrupt, @@ -33,14 +32,21 @@ import type { TokenApprovalTransactionStep } from 'uniswap/src/features/transact import type { Permit2TransactionStep } from 'uniswap/src/features/transactions/steps/permit2Transaction' import type { TokenRevocationTransactionStep } from 'uniswap/src/features/transactions/steps/revoke' import type { + HandleApprovalStepParams, + HandleOnChainPermit2TransactionStep, + HandleOnChainStepParams, + HandleSignatureStepParams, OnChainTransactionStep, - SignatureTransactionStep, TransactionStep, } from 'uniswap/src/features/transactions/steps/types' import { TransactionStepType } from 'uniswap/src/features/transactions/steps/types' import { SolanaTrade } from 'uniswap/src/features/transactions/swap/types/solana' -import type { SetCurrentStepFn } from 'uniswap/src/features/transactions/swap/types/swapCallback' -import type { BridgeTrade, ClassicTrade, UniswapXTrade } from 'uniswap/src/features/transactions/swap/types/trade' +import type { + BridgeTrade, + ChainedActionTrade, + ClassicTrade, + UniswapXTrade, +} from 'uniswap/src/features/transactions/swap/types/trade' import { isUniswapX } from 'uniswap/src/features/transactions/swap/utils/routing' import type { ApproveTransactionInfo, @@ -75,12 +81,6 @@ export enum TransactionBreadcrumbStatus { Interrupted = 'interrupted', } -export interface HandleSignatureStepParams { - account: AccountDetails - step: T - setCurrentStep: SetCurrentStepFn - ignoreInterrupt?: boolean -} export function* handleSignatureStep({ setCurrentStep, step, ignoreInterrupt, account }: HandleSignatureStepParams) { // Add a watcher to check if the transaction flow is interrupted during this step const { throwIfInterrupted } = yield* watchForInterruption(ignoreInterrupt) @@ -107,20 +107,6 @@ export function* handleSignatureStep({ setCurrentStep, step, ignoreInterrupt, ac return signature } -export interface HandleOnChainStepParams { - account: AccountDetails - info: TransactionInfo - step: T - setCurrentStep: SetCurrentStepFn - /** Controls whether the function allow submitting a duplicate tx (a tx w/ identical `info` to another recent/pending tx). Defaults to false. */ - allowDuplicativeTx?: boolean - /** Controls whether the function should throw an error upon interrupt or not, defaults to `false`. */ - ignoreInterrupt?: boolean - /** Controls whether the function should wait to return until after the transaction has confirmed. Defaults to `true`. */ - shouldWaitForConfirmation?: boolean - /** Called when data returned from a submitted transaction differs from data originally sent to the wallet. */ - onModification?: (response: VitalTxFields) => void | Generator -} export function* handleOnChainStep(params: HandleOnChainStepParams) { const { account, @@ -350,15 +336,12 @@ function transformTransactionResponse(response: TransactionResponse | Transactio return { hash: response.hash, data: response.input, nonce: response.nonce } } -interface HandlePermitStepParams extends Omit, 'info'> {} -export function* handlePermitTransactionStep(params: HandlePermitStepParams) { +export function* handlePermitTransactionStep(params: HandleOnChainPermit2TransactionStep) { const { step } = params const info = getPermitTransactionInfo(step) return yield* call(handleOnChainStep, { ...params, info }) } -interface HandleApprovalStepParams - extends Omit, 'info'> {} export function* handleApprovalTransactionStep(params: HandleApprovalStepParams) { const { step, account } = params const info = getApprovalTransactionInfo(step) @@ -528,11 +511,11 @@ export async function getSigner(account: string): Promise { type SwapInfo = ExactInputSwapTransactionInfo | ExactOutputSwapTransactionInfo export function getSwapTransactionInfo( - trade: ClassicTrade | BridgeTrade | SolanaTrade, + trade: ClassicTrade | BridgeTrade | SolanaTrade | ChainedActionTrade, ): SwapInfo | BridgeTransactionInfo export function getSwapTransactionInfo(trade: UniswapXTrade): SwapInfo & { isUniswapXOrder: true } export function getSwapTransactionInfo( - trade: ClassicTrade | BridgeTrade | UniswapXTrade | SolanaTrade, + trade: ClassicTrade | BridgeTrade | UniswapXTrade | SolanaTrade | ChainedActionTrade, ): SwapInfo | BridgeTransactionInfo { if (trade.routing === TradingApi.Routing.BRIDGE) { return { diff --git a/apps/web/src/state/sagas/transactions/wrapSaga.ts b/apps/web/src/state/sagas/transactions/wrapSaga.ts index d2b767e6076..6dc3d46a298 100644 --- a/apps/web/src/state/sagas/transactions/wrapSaga.ts +++ b/apps/web/src/state/sagas/transactions/wrapSaga.ts @@ -6,10 +6,10 @@ import { useAccount } from 'hooks/useAccount' import useSelectChain from 'hooks/useSelectChain' import { useCallback } from 'react' import { useDispatch } from 'react-redux' -import { HandleOnChainStepParams, handleOnChainStep } from 'state/sagas/transactions/utils' +import { handleOnChainStep } from 'state/sagas/transactions/utils' import { call } from 'typed-redux-saga' import { isTestnetChain } from 'uniswap/src/features/chains/utils' -import { TransactionStepType } from 'uniswap/src/features/transactions/steps/types' +import { HandleOnChainStepParams, TransactionStepType } from 'uniswap/src/features/transactions/steps/types' import { WrapTransactionStep } from 'uniswap/src/features/transactions/steps/wrap' import { WrapCallback, WrapCallbackParams } from 'uniswap/src/features/transactions/swap/types/wrapCallback' import { TransactionType, WrapTransactionInfo } from 'uniswap/src/features/transactions/types/transactionDetails' diff --git a/apps/web/src/state/swap/hooks.test.tsx b/apps/web/src/state/swap/hooks.test.tsx index 2836b4d444d..516eda349ac 100644 --- a/apps/web/src/state/swap/hooks.test.tsx +++ b/apps/web/src/state/swap/hooks.test.tsx @@ -20,8 +20,9 @@ import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledCh import { UniverseChainId } from 'uniswap/src/features/chains/types' import { CurrencyField } from 'uniswap/src/types/currency' -vi.mock('uniswap/src/features/gating/hooks', () => { +vi.mock('@universe/gating', async (importOriginal) => { return { + ...(await importOriginal()), useFeatureFlag: vi.fn(), getFeatureFlag: vi.fn(), } @@ -128,6 +129,91 @@ describe('hooks', () => { chainId: undefined, }) }) + + test('no query parameters', () => { + expect(queryParametersToCurrencyState(parse('', { parseArrays: false, ignoreQueryPrefix: true }))).toEqual({ + inputCurrencyAddress: undefined, + outputCurrencyAddress: undefined, + value: undefined, + field: undefined, + chainId: undefined, + outputChainId: undefined, + }) + }) + + test('only chain parameter, no currencies', () => { + expect( + queryParametersToCurrencyState(parse('?chain=optimism', { parseArrays: false, ignoreQueryPrefix: true })), + ).toEqual({ + inputCurrencyAddress: undefined, + outputCurrencyAddress: undefined, + value: undefined, + field: undefined, + chainId: UniverseChainId.Optimism, + outputChainId: undefined, + }) + }) + + test('only outputChain parameter, no currencies', () => { + expect( + queryParametersToCurrencyState(parse('?outputChain=base', { parseArrays: false, ignoreQueryPrefix: true })), + ).toEqual({ + inputCurrencyAddress: undefined, + outputCurrencyAddress: undefined, + value: undefined, + field: undefined, + chainId: undefined, + outputChainId: UniverseChainId.Base, + }) + }) + + test('both chain and outputChain parameters, no currencies', () => { + expect( + queryParametersToCurrencyState( + parse('?chain=mainnet&outputChain=optimism', { parseArrays: false, ignoreQueryPrefix: true }), + ), + ).toEqual({ + inputCurrencyAddress: undefined, + outputCurrencyAddress: undefined, + value: undefined, + field: undefined, + chainId: UniverseChainId.Mainnet, + outputChainId: UniverseChainId.Optimism, + }) + }) + + test('outputChain parameter with output currency', () => { + expect( + queryParametersToCurrencyState( + parse(`?outputChain=base&outputCurrency=${DAI.address}`, { parseArrays: false, ignoreQueryPrefix: true }), + ), + ).toEqual({ + inputCurrencyAddress: undefined, + outputCurrencyAddress: DAI.address, + value: undefined, + field: undefined, + chainId: undefined, + outputChainId: UniverseChainId.Base, + }) + }) + + test('both chain and outputChain with input and output currencies', () => { + expect( + queryParametersToCurrencyState( + parse(`?chain=mainnet&outputChain=optimism&inputCurrency=ETH&outputCurrency=${USDC_OPTIMISM.address}`, { + parseArrays: false, + ignoreQueryPrefix: true, + }), + ), + ).toEqual({ + inputCurrencyAddress: 'ETH', + outputCurrencyAddress: USDC_OPTIMISM.address, + value: undefined, + field: undefined, + chainId: UniverseChainId.Mainnet, + outputChainId: UniverseChainId.Optimism, + }) + }) }) describe('URL parameter serialization', () => { @@ -260,7 +346,7 @@ describe('hooks', () => { const { result: { - current: { initialInputCurrency, initialOutputCurrency, initialChainId }, + current: { initialInputCurrency, initialOutputCurrency, initialInputChainId: initialChainId }, }, } = renderHook(() => useInitialCurrencyState()) @@ -282,7 +368,7 @@ describe('hooks', () => { const { result: { - current: { initialInputCurrency, initialOutputCurrency, initialChainId }, + current: { initialInputCurrency, initialOutputCurrency, initialInputChainId: initialChainId }, }, } = renderHook(() => useInitialCurrencyState()) @@ -307,7 +393,13 @@ describe('hooks', () => { const { result: { - current: { initialInputCurrency, initialOutputCurrency, initialTypedValue, initialField, initialChainId }, + current: { + initialInputCurrency, + initialOutputCurrency, + initialTypedValue, + initialField, + initialInputChainId: initialChainId, + }, }, } = renderHook(() => useInitialCurrencyState()) @@ -328,7 +420,7 @@ describe('hooks', () => { const { result: { - current: { initialInputCurrency, initialOutputCurrency, initialChainId }, + current: { initialInputCurrency, initialOutputCurrency, initialInputChainId: initialChainId }, }, } = renderHook(() => useInitialCurrencyState()) @@ -391,7 +483,7 @@ describe('hooks', () => { const { result: { - current: { initialInputCurrency, initialChainId }, + current: { initialInputCurrency, initialInputChainId: initialChainId }, }, } = renderHook(() => useInitialCurrencyState()) @@ -417,7 +509,7 @@ describe('hooks', () => { const { result: { - current: { initialInputCurrency, initialOutputCurrency, initialChainId }, + current: { initialInputCurrency, initialOutputCurrency, initialInputChainId: initialChainId }, }, } = renderHook(() => useInitialCurrencyState()) @@ -444,7 +536,7 @@ describe('hooks', () => { const { result: { - current: { initialInputCurrency, initialChainId }, + current: { initialInputCurrency, initialInputChainId: initialChainId }, }, } = renderHook(() => useInitialCurrencyState()) @@ -463,7 +555,7 @@ describe('hooks', () => { const { result: { - current: { initialInputCurrency, initialOutputCurrency, initialChainId }, + current: { initialInputCurrency, initialOutputCurrency, initialInputChainId: initialChainId }, }, } = renderHook(() => useInitialCurrencyState()) @@ -482,7 +574,7 @@ describe('hooks', () => { const { result: { - current: { initialInputCurrency, initialOutputCurrency, initialChainId }, + current: { initialInputCurrency, initialOutputCurrency, initialInputChainId: initialChainId }, }, } = renderHook(() => useInitialCurrencyState()) @@ -504,7 +596,7 @@ describe('hooks', () => { const { result: { - current: { initialInputCurrency, initialOutputCurrency, initialChainId }, + current: { initialInputCurrency, initialOutputCurrency, initialInputChainId: initialChainId }, }, } = renderHook(() => useInitialCurrencyState()) diff --git a/apps/web/src/state/swap/hooks.tsx b/apps/web/src/state/swap/hooks.tsx index 1f590d11da1..e95c837cd79 100644 --- a/apps/web/src/state/swap/hooks.tsx +++ b/apps/web/src/state/swap/hooks.tsx @@ -225,7 +225,8 @@ export function useInitialCurrencyState(): { initialOutputCurrency?: Currency initialTypedValue?: string initialField?: CurrencyField - initialChainId: UniverseChainId + initialInputChainId?: UniverseChainId + initialOutputChainId?: UniverseChainId triggerConnect: boolean } { const { setIsUserSelectedToken } = useMultichainContext() @@ -243,7 +244,10 @@ export function useInitialCurrencyState(): { const isSupportedChainCompatible = isTestnetModeEnabled === !!supportedChainInfo.testnet const hasCurrencyQueryParams = - parsedCurrencyState.inputCurrencyAddress || parsedCurrencyState.outputCurrencyAddress || parsedCurrencyState.chainId + parsedCurrencyState.inputCurrencyAddress || + parsedCurrencyState.outputCurrencyAddress || + parsedCurrencyState.chainId || + parsedCurrencyState.outputChainId useEffect(() => { if (parsedCurrencyState.inputCurrencyAddress || parsedCurrencyState.outputCurrencyAddress) { @@ -255,9 +259,9 @@ export function useInitialCurrencyState(): { const { initialInputCurrencyAddress, initialChainId } = useMemo(() => { // Default to native if no query params or chain is not compatible with testnet or mainnet mode if (!hasCurrencyQueryParams || !isSupportedChainCompatible) { - const initialChainId = persistedFilteredChainIds?.input ?? defaultChainId + const initialChainId = persistedFilteredChainIds?.input return { - initialInputCurrencyAddress: getNativeAddress(initialChainId), + initialInputCurrencyAddress: getNativeAddress(initialChainId ?? defaultChainId), initialChainId, } } @@ -265,13 +269,13 @@ export function useInitialCurrencyState(): { if (parsedCurrencyState.inputCurrencyAddress) { return { initialInputCurrencyAddress: parsedCurrencyState.inputCurrencyAddress, - initialChainId: supportedChainId, + initialChainId: parsedCurrencyState.chainId ? supportedChainId : undefined, } } // return ETH or parsedCurrencyState return { initialInputCurrencyAddress: parsedCurrencyState.outputCurrencyAddress ? undefined : 'ETH', - initialChainId: supportedChainId, + initialChainId: parsedCurrencyState.chainId ? supportedChainId : undefined, } }, [ hasCurrencyQueryParams, @@ -282,15 +286,15 @@ export function useInitialCurrencyState(): { defaultChainId, ]) - const outputChainIsSupported = useSupportedChainId(parsedCurrencyState.outputChainId) + const supportedOutputChainId = useSupportedChainId(parsedCurrencyState.outputChainId) const initialOutputCurrencyAddress = useMemo( () => // clear output if identical unless there's a supported outputChainId which means we're bridging - initialInputCurrencyAddress === parsedCurrencyState.outputCurrencyAddress && !outputChainIsSupported + initialInputCurrencyAddress === parsedCurrencyState.outputCurrencyAddress && !supportedOutputChainId ? undefined : parsedCurrencyState.outputCurrencyAddress, - [initialInputCurrencyAddress, parsedCurrencyState.outputCurrencyAddress, outputChainIsSupported], + [initialInputCurrencyAddress, parsedCurrencyState.outputCurrencyAddress, supportedOutputChainId], ) const initialInputCurrency = useCurrency({ address: initialInputCurrencyAddress, chainId: initialChainId }) @@ -313,7 +317,8 @@ export function useInitialCurrencyState(): { initialOutputCurrency, initialTypedValue, initialField, - initialChainId, + initialInputChainId: initialChainId, + initialOutputChainId: supportedOutputChainId, triggerConnect: !!parsedQs.connect, } } diff --git a/apps/web/src/state/transactions/types.ts b/apps/web/src/state/transactions/types.ts index 70d93c70194..d5708d22270 100644 --- a/apps/web/src/state/transactions/types.ts +++ b/apps/web/src/state/transactions/types.ts @@ -8,7 +8,11 @@ import type { } from 'uniswap/src/features/transactions/types/transactionDetails' import type { ValidatedTransactionRequest } from 'uniswap/src/features/transactions/types/transactionRequests' -// Re-export for backward compatibility +/** + * Re-export for backward compatibility + * + * @deprecated Use TransactionTypeInfo + */ export type TransactionInfo = TransactionTypeInfo // Web-specific pending transaction details with guaranteed pending status diff --git a/apps/web/src/state/walletCapabilities/hooks/useMismatchAccount.ts b/apps/web/src/state/walletCapabilities/hooks/useMismatchAccount.ts index 86b48a4a479..1b5633726c5 100644 --- a/apps/web/src/state/walletCapabilities/hooks/useMismatchAccount.ts +++ b/apps/web/src/state/walletCapabilities/hooks/useMismatchAccount.ts @@ -1,4 +1,5 @@ import { nanoid } from '@reduxjs/toolkit' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { popupRegistry } from 'components/Popups/registry' import { PopupType } from 'components/Popups/types' import { useRef } from 'react' @@ -9,8 +10,6 @@ import { isAtomicBatchingSupportedByChainId } from 'state/walletCapabilities/lib import { useDelegationService } from 'state/wallets/useDelegationService' import { selectHasShownMismatchToast } from 'uniswap/src/features/behaviorHistory/selectors' import { setHasShownMismatchToast } from 'uniswap/src/features/behaviorHistory/slice' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { createHasMismatchUtil, type HasMismatchUtil } from 'uniswap/src/features/smartWallet/mismatch/mismatch' import { UniswapEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send.web' diff --git a/apps/web/src/state/walletCapabilities/hooks/useWalletGetCapabilitiesMutation.ts b/apps/web/src/state/walletCapabilities/hooks/useWalletGetCapabilitiesMutation.ts index 889cdf53524..2fca5aae612 100644 --- a/apps/web/src/state/walletCapabilities/hooks/useWalletGetCapabilitiesMutation.ts +++ b/apps/web/src/state/walletCapabilities/hooks/useWalletGetCapabilitiesMutation.ts @@ -1,11 +1,10 @@ import { queryOptions, useMutation, useQueryClient } from '@tanstack/react-query' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { useAccount } from 'hooks/useAccount' import ms from 'ms' import { useAppDispatch } from 'state/hooks' import { handleGetCapabilities } from 'state/walletCapabilities/lib/handleGetCapabilities' import { setCapabilitiesByChain, setCapabilitiesNotSupported } from 'state/walletCapabilities/reducer' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { getLogger } from 'utilities/src/logger/logger' import { useEvent } from 'utilities/src/react/hooks' import { ReactQueryCacheKey } from 'utilities/src/reactQuery/cache' diff --git a/apps/web/src/utils/computeSurroundingTicks.test.ts b/apps/web/src/utils/computeSurroundingTicks.test.ts index 1be002984db..266ddb293ca 100644 --- a/apps/web/src/utils/computeSurroundingTicks.test.ts +++ b/apps/web/src/utils/computeSurroundingTicks.test.ts @@ -1,5 +1,5 @@ import { TickData } from 'appGraphql/data/AllV3TicksQuery' -import { ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' +import { ProtocolVersion } from '@uniswap/client-data-api/dist/data/v1/poolTypes_pb' import { Price, Token } from '@uniswap/sdk-core' import { FeeAmount, TICK_SPACINGS } from '@uniswap/v3-sdk' import JSBI from 'jsbi' diff --git a/apps/web/src/utils/computeSurroundingTicks.ts b/apps/web/src/utils/computeSurroundingTicks.ts index 03f3f448e93..4f98225ae92 100644 --- a/apps/web/src/utils/computeSurroundingTicks.ts +++ b/apps/web/src/utils/computeSurroundingTicks.ts @@ -1,5 +1,5 @@ import { Ticks } from 'appGraphql/data/AllV3TicksQuery' -import { ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' +import { ProtocolVersion } from '@uniswap/client-data-api/dist/data/v1/poolTypes_pb' import { Currency, Price, Token } from '@uniswap/sdk-core' import { tickToPrice as tickToPriceV3 } from '@uniswap/v3-sdk' import { tickToPrice as tickToPriceV4 } from '@uniswap/v4-sdk' diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 8426335ca78..31f14d0e75b 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -56,6 +56,9 @@ }, { "path": "../../packages/api" + }, + { + "path": "../../packages/gating" } ] } diff --git a/apps/web/vite.config.mts b/apps/web/vite.config.mts index 31586201d5a..2f1476e4429 100644 --- a/apps/web/vite.config.mts +++ b/apps/web/vite.config.mts @@ -287,7 +287,7 @@ export default defineConfig(({ mode }) => { '@visx/responsive', ], // Libraries that shouldn't be pre-bundled - exclude: ['expo-clipboard'], + exclude: ['expo-clipboard', '@connectrpc/connect'], esbuildOptions: { resolveExtensions: ['.web.js', '.web.ts', '.web.tsx', '.js', '.ts', '.tsx'], loader: { diff --git a/bun.lock b/bun.lock index a2d98fc1fbc..d06d2a9e293 100644 --- a/bun.lock +++ b/bun.lock @@ -67,6 +67,7 @@ "@uniswap/v3-sdk": "3.25.2", "@uniswap/v4-sdk": "1.21.2", "@universe/api": "workspace:^", + "@universe/gating": "workspace:^", "@wxt-dev/module-react": "1.1.3", "confusing-browser-globals": "1.0.11", "dotenv-webpack": "8.0.1", @@ -196,6 +197,7 @@ "@uniswap/ethers-rs-mobile": "0.0.5", "@uniswap/sdk-core": "7.7.2", "@universe/api": "workspace:^", + "@universe/gating": "workspace:^", "@walletconnect/core": "2.21.4", "@walletconnect/react-native-compat": "2.21.4", "@walletconnect/types": "2.21.4", @@ -362,8 +364,8 @@ "@types/react-scroll-sync": "0.9.0", "@uniswap/analytics": "1.7.2", "@uniswap/analytics-events": "2.43.0", + "@uniswap/client-data-api": "0.0.18", "@uniswap/client-explore": "0.0.17", - "@uniswap/client-pools": "0.0.17", "@uniswap/merkle-distributor": "1.0.1", "@uniswap/permit2-sdk": "1.3.0", "@uniswap/router-sdk": "2.0.2", @@ -378,6 +380,7 @@ "@uniswap/v3-sdk": "3.25.2", "@uniswap/v4-sdk": "1.21.2", "@universe/api": "workspace:^", + "@universe/gating": "workspace:^", "@visx/group": "2.17.0", "@visx/responsive": "3.12.0", "@visx/shape": "2.18.0", @@ -584,10 +587,9 @@ "@connectrpc/connect": "1.4.0", "@connectrpc/connect-web": "1.4.0", "@tanstack/react-query": "5.77.2", - "@uniswap/client-data-api": "0.0.14", + "@uniswap/client-data-api": "0.0.18", "@uniswap/client-embeddedwallet": "0.0.16", "@uniswap/client-explore": "0.0.17", - "@uniswap/client-pools": "0.0.17", "@uniswap/client-trading": "0.1.0", "@universe/config": "workspace:^", "@universe/sessions": "workspace:^", @@ -682,6 +684,37 @@ "eslint": "8.44.0", }, }, + "packages/gating": { + "name": "@universe/gating", + "version": "0.0.0", + "dependencies": { + "@statsig/client-core": "3.12.2", + "@statsig/js-client": "3.12.2", + "@statsig/js-local-overrides": "3.12.2", + "@statsig/react-bindings": "3.12.2", + "@statsig/react-native-bindings": "3.12.2", + "@universe/api": "workspace:*", + "utilities": "workspace:*", + }, + "devDependencies": { + "@types/node": "22.13.1", + "@uniswap/eslint-config": "workspace:^", + "depcheck": "1.4.7", + "eslint": "8.44.0", + "typescript": "5.3.3", + }, + }, + "packages/notifications": { + "name": "@universe/notifications", + "version": "0.0.0", + "devDependencies": { + "@types/node": "22.13.1", + "@uniswap/eslint-config": "workspace:^", + "depcheck": "1.4.7", + "eslint": "8.44.0", + "typescript": "5.3.3", + }, + }, "packages/sessions": { "name": "@universe/sessions", "version": "0.0.0", @@ -774,6 +807,7 @@ "@connectrpc/connect-query": "1.4.1", "@datadog/browser-logs": "5.20.0", "@datadog/browser-rum": "5.23.3", + "@ethersproject/abstract-provider": "5.8.0", "@ethersproject/abstract-signer": "5.7.0", "@ethersproject/address": "5.7.0", "@ethersproject/bignumber": "5.7.0", @@ -788,21 +822,15 @@ "@shopify/flash-list": "1.7.3", "@simplewebauthn/browser": "13.1.0", "@solana/web3.js": "1.92.0", - "@statsig/client-core": "3.12.2", - "@statsig/js-client": "3.12.2", - "@statsig/js-local-overrides": "3.12.2", - "@statsig/react-bindings": "3.12.2", - "@statsig/react-native-bindings": "3.12.2", "@tanstack/query-async-storage-persister": "5.51.21", "@tanstack/react-query": "5.77.2", "@tanstack/react-query-persist-client": "5.77.2", "@typechain/ethers-v5": "7.2.0", "@types/poisson-disk-sampling": "2.2.4", "@uniswap/analytics-events": "2.43.0", - "@uniswap/client-data-api": "0.0.14", + "@uniswap/client-data-api": "0.0.18", "@uniswap/client-embeddedwallet": "0.0.16", "@uniswap/client-explore": "0.0.17", - "@uniswap/client-pools": "0.0.17", "@uniswap/client-search": "0.0.10", "@uniswap/client-trading": "0.1.0", "@uniswap/permit2-sdk": "1.3.0", @@ -815,6 +843,7 @@ "@uniswap/v4-sdk": "1.21.2", "@universe/api": "workspace:^", "@universe/config": "workspace:^", + "@universe/gating": "workspace:^", "apollo-link-rest": "0.9.0", "date-fns": "2.30.0", "dayjs": "1.11.7", @@ -971,12 +1000,13 @@ "@scure/bip32": "1.3.2", "@tanstack/react-query": "5.77.2", "@uniswap/analytics-events": "2.43.0", - "@uniswap/client-data-api": "0.0.14", + "@uniswap/client-data-api": "0.0.18", "@uniswap/permit2-sdk": "1.3.0", "@uniswap/router-sdk": "2.0.2", "@uniswap/sdk-core": "7.7.2", "@uniswap/universal-router-sdk": "4.19.5", "@universe/api": "workspace:^", + "@universe/gating": "workspace:^", "apollo3-cache-persist": "0.14.1", "dayjs": "1.11.7", "ethers": "5.7.2", @@ -1167,29 +1197,29 @@ "@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], - "@aws-sdk/client-cloudwatch-logs": ["@aws-sdk/client-cloudwatch-logs@3.911.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.911.0", "@aws-sdk/credential-provider-node": "3.911.0", "@aws-sdk/middleware-host-header": "3.910.0", "@aws-sdk/middleware-logger": "3.910.0", "@aws-sdk/middleware-recursion-detection": "3.910.0", "@aws-sdk/middleware-user-agent": "3.911.0", "@aws-sdk/region-config-resolver": "3.910.0", "@aws-sdk/types": "3.910.0", "@aws-sdk/util-endpoints": "3.910.0", "@aws-sdk/util-user-agent-browser": "3.910.0", "@aws-sdk/util-user-agent-node": "3.911.0", "@smithy/config-resolver": "^4.3.2", "@smithy/core": "^3.16.1", "@smithy/eventstream-serde-browser": "^4.2.2", "@smithy/eventstream-serde-config-resolver": "^4.3.2", "@smithy/eventstream-serde-node": "^4.2.2", "@smithy/fetch-http-handler": "^5.3.3", "@smithy/hash-node": "^4.2.2", "@smithy/invalid-dependency": "^4.2.2", "@smithy/middleware-content-length": "^4.2.2", "@smithy/middleware-endpoint": "^4.3.3", "@smithy/middleware-retry": "^4.4.3", "@smithy/middleware-serde": "^4.2.2", "@smithy/middleware-stack": "^4.2.2", "@smithy/node-config-provider": "^4.3.2", "@smithy/node-http-handler": "^4.4.1", "@smithy/protocol-http": "^5.3.2", "@smithy/smithy-client": "^4.8.1", "@smithy/types": "^4.7.1", "@smithy/url-parser": "^4.2.2", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.2", "@smithy/util-defaults-mode-node": "^4.2.3", "@smithy/util-endpoints": "^3.2.2", "@smithy/util-middleware": "^4.2.2", "@smithy/util-retry": "^4.2.2", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-172pKzqYk/GtIQsdrqmJCg8VRBGR+U70kSWdcOWtTMZOOex7Cv6iYMsCLX/ckmvVCKicgsSdxXE6TWYIEMr6oQ=="], + "@aws-sdk/client-cloudwatch-logs": ["@aws-sdk/client-cloudwatch-logs@3.913.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.911.0", "@aws-sdk/credential-provider-node": "3.913.0", "@aws-sdk/middleware-host-header": "3.910.0", "@aws-sdk/middleware-logger": "3.910.0", "@aws-sdk/middleware-recursion-detection": "3.910.0", "@aws-sdk/middleware-user-agent": "3.911.0", "@aws-sdk/region-config-resolver": "3.910.0", "@aws-sdk/types": "3.910.0", "@aws-sdk/util-endpoints": "3.910.0", "@aws-sdk/util-user-agent-browser": "3.910.0", "@aws-sdk/util-user-agent-node": "3.911.0", "@smithy/config-resolver": "^4.3.2", "@smithy/core": "^3.16.1", "@smithy/eventstream-serde-browser": "^4.2.2", "@smithy/eventstream-serde-config-resolver": "^4.3.2", "@smithy/eventstream-serde-node": "^4.2.2", "@smithy/fetch-http-handler": "^5.3.3", "@smithy/hash-node": "^4.2.2", "@smithy/invalid-dependency": "^4.2.2", "@smithy/middleware-content-length": "^4.2.2", "@smithy/middleware-endpoint": "^4.3.3", "@smithy/middleware-retry": "^4.4.3", "@smithy/middleware-serde": "^4.2.2", "@smithy/middleware-stack": "^4.2.2", "@smithy/node-config-provider": "^4.3.2", "@smithy/node-http-handler": "^4.4.1", "@smithy/protocol-http": "^5.3.2", "@smithy/smithy-client": "^4.8.1", "@smithy/types": "^4.7.1", "@smithy/url-parser": "^4.2.2", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.2", "@smithy/util-defaults-mode-node": "^4.2.3", "@smithy/util-endpoints": "^3.2.2", "@smithy/util-middleware": "^4.2.2", "@smithy/util-retry": "^4.2.2", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-ztaUBCI6ps90O5sERy5ZP8aGC2+Ks9kvOJrdpGFMKcTVtyHP1xTB/FDfNvmz2s25S8W7yeCokvs1fvoKcLyniQ=="], - "@aws-sdk/client-cognito-identity": ["@aws-sdk/client-cognito-identity@3.911.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.911.0", "@aws-sdk/credential-provider-node": "3.911.0", "@aws-sdk/middleware-host-header": "3.910.0", "@aws-sdk/middleware-logger": "3.910.0", "@aws-sdk/middleware-recursion-detection": "3.910.0", "@aws-sdk/middleware-user-agent": "3.911.0", "@aws-sdk/region-config-resolver": "3.910.0", "@aws-sdk/types": "3.910.0", "@aws-sdk/util-endpoints": "3.910.0", "@aws-sdk/util-user-agent-browser": "3.910.0", "@aws-sdk/util-user-agent-node": "3.911.0", "@smithy/config-resolver": "^4.3.2", "@smithy/core": "^3.16.1", "@smithy/fetch-http-handler": "^5.3.3", "@smithy/hash-node": "^4.2.2", "@smithy/invalid-dependency": "^4.2.2", "@smithy/middleware-content-length": "^4.2.2", "@smithy/middleware-endpoint": "^4.3.3", "@smithy/middleware-retry": "^4.4.3", "@smithy/middleware-serde": "^4.2.2", "@smithy/middleware-stack": "^4.2.2", "@smithy/node-config-provider": "^4.3.2", "@smithy/node-http-handler": "^4.4.1", "@smithy/protocol-http": "^5.3.2", "@smithy/smithy-client": "^4.8.1", "@smithy/types": "^4.7.1", "@smithy/url-parser": "^4.2.2", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.2", "@smithy/util-defaults-mode-node": "^4.2.3", "@smithy/util-endpoints": "^3.2.2", "@smithy/util-middleware": "^4.2.2", "@smithy/util-retry": "^4.2.2", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Ch/ndkyrh5fAIOqIBS/0IOSsxLQSrzhmBqyZ6Zrahy/haKHOC1UxFFld7crJUbcukvgvmuM9l5DRncy0tIe1tQ=="], + "@aws-sdk/client-cognito-identity": ["@aws-sdk/client-cognito-identity@3.913.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.911.0", "@aws-sdk/credential-provider-node": "3.913.0", "@aws-sdk/middleware-host-header": "3.910.0", "@aws-sdk/middleware-logger": "3.910.0", "@aws-sdk/middleware-recursion-detection": "3.910.0", "@aws-sdk/middleware-user-agent": "3.911.0", "@aws-sdk/region-config-resolver": "3.910.0", "@aws-sdk/types": "3.910.0", "@aws-sdk/util-endpoints": "3.910.0", "@aws-sdk/util-user-agent-browser": "3.910.0", "@aws-sdk/util-user-agent-node": "3.911.0", "@smithy/config-resolver": "^4.3.2", "@smithy/core": "^3.16.1", "@smithy/fetch-http-handler": "^5.3.3", "@smithy/hash-node": "^4.2.2", "@smithy/invalid-dependency": "^4.2.2", "@smithy/middleware-content-length": "^4.2.2", "@smithy/middleware-endpoint": "^4.3.3", "@smithy/middleware-retry": "^4.4.3", "@smithy/middleware-serde": "^4.2.2", "@smithy/middleware-stack": "^4.2.2", "@smithy/node-config-provider": "^4.3.2", "@smithy/node-http-handler": "^4.4.1", "@smithy/protocol-http": "^5.3.2", "@smithy/smithy-client": "^4.8.1", "@smithy/types": "^4.7.1", "@smithy/url-parser": "^4.2.2", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.2", "@smithy/util-defaults-mode-node": "^4.2.3", "@smithy/util-endpoints": "^3.2.2", "@smithy/util-middleware": "^4.2.2", "@smithy/util-retry": "^4.2.2", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-TdEwasoXnLIb90z7NL1vLbEprzY0vdRqZH97ubIUDo8EaJ6WrJ35Um5g0rcnWKR6C+P9lKKI4mVv2BI2EwY94Q=="], - "@aws-sdk/client-iam": ["@aws-sdk/client-iam@3.911.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.911.0", "@aws-sdk/credential-provider-node": "3.911.0", "@aws-sdk/middleware-host-header": "3.910.0", "@aws-sdk/middleware-logger": "3.910.0", "@aws-sdk/middleware-recursion-detection": "3.910.0", "@aws-sdk/middleware-user-agent": "3.911.0", "@aws-sdk/region-config-resolver": "3.910.0", "@aws-sdk/types": "3.910.0", "@aws-sdk/util-endpoints": "3.910.0", "@aws-sdk/util-user-agent-browser": "3.910.0", "@aws-sdk/util-user-agent-node": "3.911.0", "@smithy/config-resolver": "^4.3.2", "@smithy/core": "^3.16.1", "@smithy/fetch-http-handler": "^5.3.3", "@smithy/hash-node": "^4.2.2", "@smithy/invalid-dependency": "^4.2.2", "@smithy/middleware-content-length": "^4.2.2", "@smithy/middleware-endpoint": "^4.3.3", "@smithy/middleware-retry": "^4.4.3", "@smithy/middleware-serde": "^4.2.2", "@smithy/middleware-stack": "^4.2.2", "@smithy/node-config-provider": "^4.3.2", "@smithy/node-http-handler": "^4.4.1", "@smithy/protocol-http": "^5.3.2", "@smithy/smithy-client": "^4.8.1", "@smithy/types": "^4.7.1", "@smithy/url-parser": "^4.2.2", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.2", "@smithy/util-defaults-mode-node": "^4.2.3", "@smithy/util-endpoints": "^3.2.2", "@smithy/util-middleware": "^4.2.2", "@smithy/util-retry": "^4.2.2", "@smithy/util-utf8": "^4.2.0", "@smithy/util-waiter": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-IksVAEDAsiKR0NsP6b4OLhtvg6GkH/CpopR8Dh1TaBTotS/lE8amF2N94SYq+emG0vJ23CvErmkHQpa2ZBYDUg=="], + "@aws-sdk/client-iam": ["@aws-sdk/client-iam@3.913.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.911.0", "@aws-sdk/credential-provider-node": "3.913.0", "@aws-sdk/middleware-host-header": "3.910.0", "@aws-sdk/middleware-logger": "3.910.0", "@aws-sdk/middleware-recursion-detection": "3.910.0", "@aws-sdk/middleware-user-agent": "3.911.0", "@aws-sdk/region-config-resolver": "3.910.0", "@aws-sdk/types": "3.910.0", "@aws-sdk/util-endpoints": "3.910.0", "@aws-sdk/util-user-agent-browser": "3.910.0", "@aws-sdk/util-user-agent-node": "3.911.0", "@smithy/config-resolver": "^4.3.2", "@smithy/core": "^3.16.1", "@smithy/fetch-http-handler": "^5.3.3", "@smithy/hash-node": "^4.2.2", "@smithy/invalid-dependency": "^4.2.2", "@smithy/middleware-content-length": "^4.2.2", "@smithy/middleware-endpoint": "^4.3.3", "@smithy/middleware-retry": "^4.4.3", "@smithy/middleware-serde": "^4.2.2", "@smithy/middleware-stack": "^4.2.2", "@smithy/node-config-provider": "^4.3.2", "@smithy/node-http-handler": "^4.4.1", "@smithy/protocol-http": "^5.3.2", "@smithy/smithy-client": "^4.8.1", "@smithy/types": "^4.7.1", "@smithy/url-parser": "^4.2.2", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.2", "@smithy/util-defaults-mode-node": "^4.2.3", "@smithy/util-endpoints": "^3.2.2", "@smithy/util-middleware": "^4.2.2", "@smithy/util-retry": "^4.2.2", "@smithy/util-utf8": "^4.2.0", "@smithy/util-waiter": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-3UR3EJ/2eutWpcs6LdtLl4JTJ4u/TZZEoLryUConEchrBNNtSVBG2CXrG7In1hS4l0t5TlkW/ruEZyLZJiqFfw=="], - "@aws-sdk/client-lambda": ["@aws-sdk/client-lambda@3.911.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.911.0", "@aws-sdk/credential-provider-node": "3.911.0", "@aws-sdk/middleware-host-header": "3.910.0", "@aws-sdk/middleware-logger": "3.910.0", "@aws-sdk/middleware-recursion-detection": "3.910.0", "@aws-sdk/middleware-user-agent": "3.911.0", "@aws-sdk/region-config-resolver": "3.910.0", "@aws-sdk/types": "3.910.0", "@aws-sdk/util-endpoints": "3.910.0", "@aws-sdk/util-user-agent-browser": "3.910.0", "@aws-sdk/util-user-agent-node": "3.911.0", "@smithy/config-resolver": "^4.3.2", "@smithy/core": "^3.16.1", "@smithy/eventstream-serde-browser": "^4.2.2", "@smithy/eventstream-serde-config-resolver": "^4.3.2", "@smithy/eventstream-serde-node": "^4.2.2", "@smithy/fetch-http-handler": "^5.3.3", "@smithy/hash-node": "^4.2.2", "@smithy/invalid-dependency": "^4.2.2", "@smithy/middleware-content-length": "^4.2.2", "@smithy/middleware-endpoint": "^4.3.3", "@smithy/middleware-retry": "^4.4.3", "@smithy/middleware-serde": "^4.2.2", "@smithy/middleware-stack": "^4.2.2", "@smithy/node-config-provider": "^4.3.2", "@smithy/node-http-handler": "^4.4.1", "@smithy/protocol-http": "^5.3.2", "@smithy/smithy-client": "^4.8.1", "@smithy/types": "^4.7.1", "@smithy/url-parser": "^4.2.2", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.2", "@smithy/util-defaults-mode-node": "^4.2.3", "@smithy/util-endpoints": "^3.2.2", "@smithy/util-middleware": "^4.2.2", "@smithy/util-retry": "^4.2.2", "@smithy/util-stream": "^4.5.2", "@smithy/util-utf8": "^4.2.0", "@smithy/util-waiter": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-cKRF10Fks0EEkZiRYlIy3mOUKCwtGET6CwsdYbaQ28qCm/Hh26QcL5YjVbq1fUF4BfdsFi7AQAX9WOWOAA8HQA=="], + "@aws-sdk/client-lambda": ["@aws-sdk/client-lambda@3.913.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.911.0", "@aws-sdk/credential-provider-node": "3.913.0", "@aws-sdk/middleware-host-header": "3.910.0", "@aws-sdk/middleware-logger": "3.910.0", "@aws-sdk/middleware-recursion-detection": "3.910.0", "@aws-sdk/middleware-user-agent": "3.911.0", "@aws-sdk/region-config-resolver": "3.910.0", "@aws-sdk/types": "3.910.0", "@aws-sdk/util-endpoints": "3.910.0", "@aws-sdk/util-user-agent-browser": "3.910.0", "@aws-sdk/util-user-agent-node": "3.911.0", "@smithy/config-resolver": "^4.3.2", "@smithy/core": "^3.16.1", "@smithy/eventstream-serde-browser": "^4.2.2", "@smithy/eventstream-serde-config-resolver": "^4.3.2", "@smithy/eventstream-serde-node": "^4.2.2", "@smithy/fetch-http-handler": "^5.3.3", "@smithy/hash-node": "^4.2.2", "@smithy/invalid-dependency": "^4.2.2", "@smithy/middleware-content-length": "^4.2.2", "@smithy/middleware-endpoint": "^4.3.3", "@smithy/middleware-retry": "^4.4.3", "@smithy/middleware-serde": "^4.2.2", "@smithy/middleware-stack": "^4.2.2", "@smithy/node-config-provider": "^4.3.2", "@smithy/node-http-handler": "^4.4.1", "@smithy/protocol-http": "^5.3.2", "@smithy/smithy-client": "^4.8.1", "@smithy/types": "^4.7.1", "@smithy/url-parser": "^4.2.2", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.2", "@smithy/util-defaults-mode-node": "^4.2.3", "@smithy/util-endpoints": "^3.2.2", "@smithy/util-middleware": "^4.2.2", "@smithy/util-retry": "^4.2.2", "@smithy/util-stream": "^4.5.2", "@smithy/util-utf8": "^4.2.0", "@smithy/util-waiter": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-Z1POaV/q+unF0G6PEDO0p6JrepJI6DXkVAl4RQiN1hZRshsfZxPQtsanKqMHLEi/OyhqnoVxy1buSNHjuumTdg=="], - "@aws-sdk/client-sfn": ["@aws-sdk/client-sfn@3.911.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.911.0", "@aws-sdk/credential-provider-node": "3.911.0", "@aws-sdk/middleware-host-header": "3.910.0", "@aws-sdk/middleware-logger": "3.910.0", "@aws-sdk/middleware-recursion-detection": "3.910.0", "@aws-sdk/middleware-user-agent": "3.911.0", "@aws-sdk/region-config-resolver": "3.910.0", "@aws-sdk/types": "3.910.0", "@aws-sdk/util-endpoints": "3.910.0", "@aws-sdk/util-user-agent-browser": "3.910.0", "@aws-sdk/util-user-agent-node": "3.911.0", "@smithy/config-resolver": "^4.3.2", "@smithy/core": "^3.16.1", "@smithy/fetch-http-handler": "^5.3.3", "@smithy/hash-node": "^4.2.2", "@smithy/invalid-dependency": "^4.2.2", "@smithy/middleware-content-length": "^4.2.2", "@smithy/middleware-endpoint": "^4.3.3", "@smithy/middleware-retry": "^4.4.3", "@smithy/middleware-serde": "^4.2.2", "@smithy/middleware-stack": "^4.2.2", "@smithy/node-config-provider": "^4.3.2", "@smithy/node-http-handler": "^4.4.1", "@smithy/protocol-http": "^5.3.2", "@smithy/smithy-client": "^4.8.1", "@smithy/types": "^4.7.1", "@smithy/url-parser": "^4.2.2", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.2", "@smithy/util-defaults-mode-node": "^4.2.3", "@smithy/util-endpoints": "^3.2.2", "@smithy/util-middleware": "^4.2.2", "@smithy/util-retry": "^4.2.2", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-JQrrhHXecw607oAqJOmg53pd9f9iYOAXcDcTfIVdHiTnXvhMshqgW8TuRobQ4n8Yg4FtgCg0SB/WQNDfYiU+vg=="], + "@aws-sdk/client-sfn": ["@aws-sdk/client-sfn@3.913.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.911.0", "@aws-sdk/credential-provider-node": "3.913.0", "@aws-sdk/middleware-host-header": "3.910.0", "@aws-sdk/middleware-logger": "3.910.0", "@aws-sdk/middleware-recursion-detection": "3.910.0", "@aws-sdk/middleware-user-agent": "3.911.0", "@aws-sdk/region-config-resolver": "3.910.0", "@aws-sdk/types": "3.910.0", "@aws-sdk/util-endpoints": "3.910.0", "@aws-sdk/util-user-agent-browser": "3.910.0", "@aws-sdk/util-user-agent-node": "3.911.0", "@smithy/config-resolver": "^4.3.2", "@smithy/core": "^3.16.1", "@smithy/fetch-http-handler": "^5.3.3", "@smithy/hash-node": "^4.2.2", "@smithy/invalid-dependency": "^4.2.2", "@smithy/middleware-content-length": "^4.2.2", "@smithy/middleware-endpoint": "^4.3.3", "@smithy/middleware-retry": "^4.4.3", "@smithy/middleware-serde": "^4.2.2", "@smithy/middleware-stack": "^4.2.2", "@smithy/node-config-provider": "^4.3.2", "@smithy/node-http-handler": "^4.4.1", "@smithy/protocol-http": "^5.3.2", "@smithy/smithy-client": "^4.8.1", "@smithy/types": "^4.7.1", "@smithy/url-parser": "^4.2.2", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.2", "@smithy/util-defaults-mode-node": "^4.2.3", "@smithy/util-endpoints": "^3.2.2", "@smithy/util-middleware": "^4.2.2", "@smithy/util-retry": "^4.2.2", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-XVonl1v6mqz+FxevEvY+CBEOi+fTG3Ht2BTwWRM3F0XUwOo4ONH4+jo54rEEbq12zVW3M53k+rMfUDyy1pSLiw=="], "@aws-sdk/client-sso": ["@aws-sdk/client-sso@3.911.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.911.0", "@aws-sdk/middleware-host-header": "3.910.0", "@aws-sdk/middleware-logger": "3.910.0", "@aws-sdk/middleware-recursion-detection": "3.910.0", "@aws-sdk/middleware-user-agent": "3.911.0", "@aws-sdk/region-config-resolver": "3.910.0", "@aws-sdk/types": "3.910.0", "@aws-sdk/util-endpoints": "3.910.0", "@aws-sdk/util-user-agent-browser": "3.910.0", "@aws-sdk/util-user-agent-node": "3.911.0", "@smithy/config-resolver": "^4.3.2", "@smithy/core": "^3.16.1", "@smithy/fetch-http-handler": "^5.3.3", "@smithy/hash-node": "^4.2.2", "@smithy/invalid-dependency": "^4.2.2", "@smithy/middleware-content-length": "^4.2.2", "@smithy/middleware-endpoint": "^4.3.3", "@smithy/middleware-retry": "^4.4.3", "@smithy/middleware-serde": "^4.2.2", "@smithy/middleware-stack": "^4.2.2", "@smithy/node-config-provider": "^4.3.2", "@smithy/node-http-handler": "^4.4.1", "@smithy/protocol-http": "^5.3.2", "@smithy/smithy-client": "^4.8.1", "@smithy/types": "^4.7.1", "@smithy/url-parser": "^4.2.2", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.2", "@smithy/util-defaults-mode-node": "^4.2.3", "@smithy/util-endpoints": "^3.2.2", "@smithy/util-middleware": "^4.2.2", "@smithy/util-retry": "^4.2.2", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-N9QAeMvN3D1ZyKXkQp4aUgC4wUMuA5E1HuVCkajc0bq1pnH4PIke36YlrDGGREqPlyLFrXCkws2gbL5p23vtlg=="], "@aws-sdk/core": ["@aws-sdk/core@3.911.0", "", { "dependencies": { "@aws-sdk/types": "3.910.0", "@aws-sdk/xml-builder": "3.911.0", "@smithy/core": "^3.16.1", "@smithy/node-config-provider": "^4.3.2", "@smithy/property-provider": "^4.2.2", "@smithy/protocol-http": "^5.3.2", "@smithy/signature-v4": "^5.3.2", "@smithy/smithy-client": "^4.8.1", "@smithy/types": "^4.7.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.2", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-k4QG9A+UCq/qlDJFmjozo6R0eXXfe++/KnCDMmajehIE9kh+b/5DqlGvAmbl9w4e92LOtrY6/DN3mIX1xs4sXw=="], - "@aws-sdk/credential-provider-cognito-identity": ["@aws-sdk/credential-provider-cognito-identity@3.911.0", "", { "dependencies": { "@aws-sdk/client-cognito-identity": "3.911.0", "@aws-sdk/types": "3.910.0", "@smithy/property-provider": "^4.2.2", "@smithy/types": "^4.7.1", "tslib": "^2.6.2" } }, "sha512-4RF/HQ2C4K+UfNfddw3xHLqk/c1G0/8nhgW10BGU0w/EICkCxtVEzgbflGeUumuXsxJYo8Fyyg/Pd8302brfHA=="], + "@aws-sdk/credential-provider-cognito-identity": ["@aws-sdk/credential-provider-cognito-identity@3.913.0", "", { "dependencies": { "@aws-sdk/client-cognito-identity": "3.913.0", "@aws-sdk/types": "3.910.0", "@smithy/property-provider": "^4.2.2", "@smithy/types": "^4.7.1", "tslib": "^2.6.2" } }, "sha512-AYZNpy3eEFzopzntLcrkEQQ1qyhg0V7BL8U77QdLSYtzoYvI9CqnWOGdWnNSEUp+Mpbk1VJyPzVfkDoDq5kX6g=="], "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.911.0", "", { "dependencies": { "@aws-sdk/core": "3.911.0", "@aws-sdk/types": "3.910.0", "@smithy/property-provider": "^4.2.2", "@smithy/types": "^4.7.1", "tslib": "^2.6.2" } }, "sha512-6FWRwWn3LUZzLhqBXB+TPMW2ijCWUqGICSw8bVakEdODrvbiv1RT/MVUayzFwz/ek6e6NKZn6DbSWzx07N9Hjw=="], "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.911.0", "", { "dependencies": { "@aws-sdk/core": "3.911.0", "@aws-sdk/types": "3.910.0", "@smithy/fetch-http-handler": "^5.3.3", "@smithy/node-http-handler": "^4.4.1", "@smithy/property-provider": "^4.2.2", "@smithy/protocol-http": "^5.3.2", "@smithy/smithy-client": "^4.8.1", "@smithy/types": "^4.7.1", "@smithy/util-stream": "^4.5.2", "tslib": "^2.6.2" } }, "sha512-xUlwKmIUW2fWP/eM3nF5u4CyLtOtyohlhGJ5jdsJokr3MrQ7w0tDITO43C9IhCn+28D5UbaiWnKw5ntkw7aVfA=="], - "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.911.0", "", { "dependencies": { "@aws-sdk/core": "3.911.0", "@aws-sdk/credential-provider-env": "3.911.0", "@aws-sdk/credential-provider-http": "3.911.0", "@aws-sdk/credential-provider-process": "3.911.0", "@aws-sdk/credential-provider-sso": "3.911.0", "@aws-sdk/credential-provider-web-identity": "3.911.0", "@aws-sdk/nested-clients": "3.911.0", "@aws-sdk/types": "3.910.0", "@smithy/credential-provider-imds": "^4.2.2", "@smithy/property-provider": "^4.2.2", "@smithy/shared-ini-file-loader": "^4.3.2", "@smithy/types": "^4.7.1", "tslib": "^2.6.2" } }, "sha512-bQ86kWAZ0Imn7uWl7uqOYZ2aqlkftPmEc8cQh+QyhmUXbia8II4oYKq/tMek6j3M5UOMCiJVxzJoxemJZA6/sw=="], + "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.913.0", "", { "dependencies": { "@aws-sdk/core": "3.911.0", "@aws-sdk/credential-provider-env": "3.911.0", "@aws-sdk/credential-provider-http": "3.911.0", "@aws-sdk/credential-provider-process": "3.911.0", "@aws-sdk/credential-provider-sso": "3.911.0", "@aws-sdk/credential-provider-web-identity": "3.911.0", "@aws-sdk/nested-clients": "3.911.0", "@aws-sdk/types": "3.910.0", "@smithy/credential-provider-imds": "^4.2.2", "@smithy/property-provider": "^4.2.2", "@smithy/shared-ini-file-loader": "^4.3.2", "@smithy/types": "^4.7.1", "tslib": "^2.6.2" } }, "sha512-iR4c4NQ1OSRKQi0SxzpwD+wP1fCy+QNKtEyCajuVlD0pvmoIHdrm5THK9e+2/7/SsQDRhOXHJfLGxHapD74WJw=="], - "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.911.0", "", { "dependencies": { "@aws-sdk/credential-provider-env": "3.911.0", "@aws-sdk/credential-provider-http": "3.911.0", "@aws-sdk/credential-provider-ini": "3.911.0", "@aws-sdk/credential-provider-process": "3.911.0", "@aws-sdk/credential-provider-sso": "3.911.0", "@aws-sdk/credential-provider-web-identity": "3.911.0", "@aws-sdk/types": "3.910.0", "@smithy/credential-provider-imds": "^4.2.2", "@smithy/property-provider": "^4.2.2", "@smithy/shared-ini-file-loader": "^4.3.2", "@smithy/types": "^4.7.1", "tslib": "^2.6.2" } }, "sha512-4oGpLwgQCKNtVoJROztJ4v7lZLhCqcUMX6pe/DQ2aU0TktZX7EczMCIEGjVo5b7yHwSNWt2zW0tDdgVUTsMHPw=="], + "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.913.0", "", { "dependencies": { "@aws-sdk/credential-provider-env": "3.911.0", "@aws-sdk/credential-provider-http": "3.911.0", "@aws-sdk/credential-provider-ini": "3.913.0", "@aws-sdk/credential-provider-process": "3.911.0", "@aws-sdk/credential-provider-sso": "3.911.0", "@aws-sdk/credential-provider-web-identity": "3.911.0", "@aws-sdk/types": "3.910.0", "@smithy/credential-provider-imds": "^4.2.2", "@smithy/property-provider": "^4.2.2", "@smithy/shared-ini-file-loader": "^4.3.2", "@smithy/types": "^4.7.1", "tslib": "^2.6.2" } }, "sha512-HQPLkKDxS83Q/nZKqg9bq4igWzYQeOMqhpx5LYs4u1GwsKeCsYrrfz12Iu4IHNWPp9EnGLcmdfbfYuqZGrsaSQ=="], "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.911.0", "", { "dependencies": { "@aws-sdk/core": "3.911.0", "@aws-sdk/types": "3.910.0", "@smithy/property-provider": "^4.2.2", "@smithy/shared-ini-file-loader": "^4.3.2", "@smithy/types": "^4.7.1", "tslib": "^2.6.2" } }, "sha512-mKshhV5jRQffZjbK9x7bs+uC2IsYKfpzYaBamFsEov3xtARCpOiKaIlM8gYKFEbHT2M+1R3rYYlhhl9ndVWS2g=="], @@ -1197,7 +1227,7 @@ "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.911.0", "", { "dependencies": { "@aws-sdk/core": "3.911.0", "@aws-sdk/nested-clients": "3.911.0", "@aws-sdk/types": "3.910.0", "@smithy/property-provider": "^4.2.2", "@smithy/shared-ini-file-loader": "^4.3.2", "@smithy/types": "^4.7.1", "tslib": "^2.6.2" } }, "sha512-urIbXWWG+cm54RwwTFQuRwPH0WPsMFSDF2/H9qO2J2fKoHRURuyblFCyYG3aVKZGvFBhOizJYexf5+5w3CJKBw=="], - "@aws-sdk/credential-providers": ["@aws-sdk/credential-providers@3.911.0", "", { "dependencies": { "@aws-sdk/client-cognito-identity": "3.911.0", "@aws-sdk/core": "3.911.0", "@aws-sdk/credential-provider-cognito-identity": "3.911.0", "@aws-sdk/credential-provider-env": "3.911.0", "@aws-sdk/credential-provider-http": "3.911.0", "@aws-sdk/credential-provider-ini": "3.911.0", "@aws-sdk/credential-provider-node": "3.911.0", "@aws-sdk/credential-provider-process": "3.911.0", "@aws-sdk/credential-provider-sso": "3.911.0", "@aws-sdk/credential-provider-web-identity": "3.911.0", "@aws-sdk/nested-clients": "3.911.0", "@aws-sdk/types": "3.910.0", "@smithy/config-resolver": "^4.3.2", "@smithy/core": "^3.16.1", "@smithy/credential-provider-imds": "^4.2.2", "@smithy/node-config-provider": "^4.3.2", "@smithy/property-provider": "^4.2.2", "@smithy/types": "^4.7.1", "tslib": "^2.6.2" } }, "sha512-BTJyah0hB0w4kP6RKBr4oA1O9cJ5hG3UWVXKIH3YvvSEfZtjbaN1lrnN9DXk1lIEsNZG/yG5m6UjI4e9c7eeKA=="], + "@aws-sdk/credential-providers": ["@aws-sdk/credential-providers@3.913.0", "", { "dependencies": { "@aws-sdk/client-cognito-identity": "3.913.0", "@aws-sdk/core": "3.911.0", "@aws-sdk/credential-provider-cognito-identity": "3.913.0", "@aws-sdk/credential-provider-env": "3.911.0", "@aws-sdk/credential-provider-http": "3.911.0", "@aws-sdk/credential-provider-ini": "3.913.0", "@aws-sdk/credential-provider-node": "3.913.0", "@aws-sdk/credential-provider-process": "3.911.0", "@aws-sdk/credential-provider-sso": "3.911.0", "@aws-sdk/credential-provider-web-identity": "3.911.0", "@aws-sdk/nested-clients": "3.911.0", "@aws-sdk/types": "3.910.0", "@smithy/config-resolver": "^4.3.2", "@smithy/core": "^3.16.1", "@smithy/credential-provider-imds": "^4.2.2", "@smithy/node-config-provider": "^4.3.2", "@smithy/property-provider": "^4.2.2", "@smithy/types": "^4.7.1", "tslib": "^2.6.2" } }, "sha512-KnkvoLXGszXNV7IMLdUH2Smo+tr4MiHUp2zkkrhl+6uXdSWpEAhlARSA8OPIxgVMabUW1AWDumN7Km7z0GvnWg=="], "@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.910.0", "", { "dependencies": { "@aws-sdk/types": "3.910.0", "@smithy/protocol-http": "^5.3.2", "@smithy/types": "^4.7.1", "tslib": "^2.6.2" } }, "sha512-F9Lqeu80/aTM6S/izZ8RtwSmjfhWjIuxX61LX+/9mxJyEkgaECRxv0chsLQsLHJumkGnXRy/eIyMLBhcTPF5vg=="], @@ -2679,7 +2709,7 @@ "@reown/walletkit": ["@reown/walletkit@1.2.8", "", { "dependencies": { "@walletconnect/core": "2.21.4", "@walletconnect/jsonrpc-provider": "1.0.14", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/logger": "2.1.2", "@walletconnect/sign-client": "2.21.4", "@walletconnect/types": "2.21.4", "@walletconnect/utils": "2.21.4" } }, "sha512-X3EO9P6+Dvc++h8OwpBtBhGmq+890UlG/o0Ilb98l5ByDr3QVcYOURRIPVcV6pkTJ9sE6sVDXW7RIRiYSnQp2g=="], - "@repeaterjs/repeater": ["@repeaterjs/repeater@3.0.6", "", {}, "sha512-Javneu5lsuhwNCryN+pXH93VPQ8g0dBX7wItHFgYiwQmzE1sVdg5tWHiOgHywzL2W21XQopa7IwIEnNbmeUJYA=="], + "@repeaterjs/repeater": ["@repeaterjs/repeater@3.0.4", "", {}, "sha512-AW8PKd6iX3vAZ0vA43nOUOnbq/X5ihgU+mSXXqunMkeQADGiqw/PY0JNeYtD5sr0PAy51YPgAPbDoeapv9r8WA=="], "@resvg/resvg-wasm": ["@resvg/resvg-wasm@2.4.1", "", {}, "sha512-yi6R0HyHtsoWTRA06Col4WoDs7SvlXU3DLMNP2bdAgs7HK18dTEVl1weXgxRzi8gwLteGUbIg29zulxIB3GSdg=="], @@ -2901,16 +2931,24 @@ "@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="], - "@solana-mobile/mobile-wallet-adapter-protocol": ["@solana-mobile/mobile-wallet-adapter-protocol@2.2.4", "", { "dependencies": { "@solana/wallet-standard": "^1.1.2", "@solana/wallet-standard-util": "^1.1.1", "@wallet-standard/core": "^1.0.3", "js-base64": "^3.7.5" }, "peerDependencies": { "react-native": ">0.69" } }, "sha512-0YvA8QAzMQYujYq1fuJ4wNlouvnJpVYJ4XKqBBh+G8IQGEezhWjuP6DryIg9gw3LD6ju/rDX1jfzGOZ38JAzkQ=="], + "@solana-mobile/mobile-wallet-adapter-protocol": ["@solana-mobile/mobile-wallet-adapter-protocol@2.2.5", "", { "dependencies": { "@solana/codecs-strings": "^4.0.0", "@solana/wallet-standard": "^1.1.2", "@solana/wallet-standard-util": "^1.1.1", "@wallet-standard/core": "^1.0.3", "js-base64": "^3.7.5" }, "peerDependencies": { "react-native": ">0.69" } }, "sha512-kCI+0/umWm98M9g12ndpS56U6wBzq4XdhobCkDPF8qRDYX/iTU8CD+QMcalh7VgRT7GWEmySQvQdaugM0Chf0g=="], - "@solana-mobile/mobile-wallet-adapter-protocol-web3js": ["@solana-mobile/mobile-wallet-adapter-protocol-web3js@2.2.4", "", { "dependencies": { "@solana-mobile/mobile-wallet-adapter-protocol": "^2.2.4", "bs58": "^5.0.0", "js-base64": "^3.7.5" }, "peerDependencies": { "@solana/web3.js": "^1.58.0" } }, "sha512-vSsIVGEOs+IJ8+5gzSwl5XBCW1zFIwhF0Qfx+fqH8F0eN5ip+XExFcnt5Of426HVpmVL2H8jocBwGwvdrTNU/A=="], + "@solana-mobile/mobile-wallet-adapter-protocol-web3js": ["@solana-mobile/mobile-wallet-adapter-protocol-web3js@2.2.5", "", { "dependencies": { "@solana-mobile/mobile-wallet-adapter-protocol": "^2.2.5", "bs58": "^5.0.0", "js-base64": "^3.7.5" }, "peerDependencies": { "@solana/web3.js": "^1.58.0" } }, "sha512-xfQl6Kee0ZXagUG5mpy+bMhQTNf2LAzF65m5SSgNJp47y/nP9GdXWi9blVH8IPP+QjF/+DnCtURaXS14bk3WJw=="], - "@solana-mobile/wallet-adapter-mobile": ["@solana-mobile/wallet-adapter-mobile@2.2.4", "", { "dependencies": { "@solana-mobile/mobile-wallet-adapter-protocol-web3js": "^2.2.0", "@solana-mobile/wallet-standard-mobile": "^0.4.1", "@solana/wallet-adapter-base": "^0.9.23", "@solana/wallet-standard-features": "^1.2.0", "js-base64": "^3.7.5" }, "optionalDependencies": { "@react-native-async-storage/async-storage": "^1.17.7" }, "peerDependencies": { "@solana/web3.js": "^1.58.0" } }, "sha512-ZKj8xU1bOtgHMgMfJh8qfUtdp5Ii4JhVJP3jqaRswYpRClmTApkBB++izSD3NBQ6fmiGv2G8F7AILQO0dYOwbg=="], + "@solana-mobile/wallet-adapter-mobile": ["@solana-mobile/wallet-adapter-mobile@2.2.5", "", { "dependencies": { "@solana-mobile/mobile-wallet-adapter-protocol-web3js": "^2.2.5", "@solana-mobile/wallet-standard-mobile": "^0.4.3", "@solana/wallet-adapter-base": "^0.9.23", "@solana/wallet-standard-features": "^1.2.0", "js-base64": "^3.7.5" }, "optionalDependencies": { "@react-native-async-storage/async-storage": "^1.17.7" }, "peerDependencies": { "@solana/web3.js": "^1.58.0" } }, "sha512-Zpzfwm3N4FfI63ZMs2qZChQ1j0z+p2prkZbSU51NyTnE+K9l9sDAl8RmRCOWnE29y+/AN10WuQZQoIAccHVOFg=="], - "@solana-mobile/wallet-standard-mobile": ["@solana-mobile/wallet-standard-mobile@0.4.2", "", { "dependencies": { "@solana-mobile/mobile-wallet-adapter-protocol": "^2.2.4", "@solana/wallet-standard-chains": "^1.1.0", "@solana/wallet-standard-features": "^1.2.0", "@wallet-standard/base": "^1.0.1", "@wallet-standard/features": "^1.0.3", "bs58": "^5.0.0", "js-base64": "^3.7.5", "qrcode": "^1.5.4" } }, "sha512-D/ebTRcpSEdCxfp7OZ0NRg+ScguJHqp208EGWI1R5rMBoGdoeu4ZvIi3VeJdi+Y9qcJFji8p2gf/wdHRL+6RkQ=="], + "@solana-mobile/wallet-standard-mobile": ["@solana-mobile/wallet-standard-mobile@0.4.3", "", { "dependencies": { "@solana-mobile/mobile-wallet-adapter-protocol": "^2.2.5", "@solana/wallet-standard-chains": "^1.1.0", "@solana/wallet-standard-features": "^1.2.0", "@wallet-standard/base": "^1.0.1", "@wallet-standard/features": "^1.0.3", "bs58": "^5.0.0", "js-base64": "^3.7.5", "qrcode": "^1.5.4" } }, "sha512-LLMQs/KgRZpftIhwOLCM2VZLMdA2vIghJjKsYUIiy1FBJS9GEkGDLJdbujb92lfAdmYwbyTuolIRik7JMPH3Kg=="], "@solana/buffer-layout": ["@solana/buffer-layout@4.0.1", "", { "dependencies": { "buffer": "~6.0.3" } }, "sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA=="], + "@solana/codecs-core": ["@solana/codecs-core@4.0.0", "", { "dependencies": { "@solana/errors": "4.0.0" }, "peerDependencies": { "typescript": ">=5.3.3" } }, "sha512-28kNUsyIlhU3MO3/7ZLDqeJf2YAm32B4tnTjl5A9HrbBqsTZ+upT/RzxZGP1MMm7jnPuIKCMwmTpsyqyR6IUpw=="], + + "@solana/codecs-numbers": ["@solana/codecs-numbers@4.0.0", "", { "dependencies": { "@solana/codecs-core": "4.0.0", "@solana/errors": "4.0.0" }, "peerDependencies": { "typescript": ">=5.3.3" } }, "sha512-z9zpjtcwzqT9rbkKVZpkWB5/0V7+6YRKs6BccHkGJlaDx8Pe/+XOvPi2rEdXPqrPd9QWb5Xp1iBfcgaDMyiOiA=="], + + "@solana/codecs-strings": ["@solana/codecs-strings@4.0.0", "", { "dependencies": { "@solana/codecs-core": "4.0.0", "@solana/codecs-numbers": "4.0.0", "@solana/errors": "4.0.0" }, "peerDependencies": { "fastestsmallesttextencoderdecoder": "^1.0.22", "typescript": ">=5.3.3" } }, "sha512-XvyD+sQ1zyA0amfxbpoFZsucLoe+yASQtDiLUGMDg5TZ82IHE3B7n82jE8d8cTAqi0HgqQiwU13snPhvg1O0Ow=="], + + "@solana/errors": ["@solana/errors@4.0.0", "", { "dependencies": { "chalk": "5.6.2", "commander": "14.0.1" }, "peerDependencies": { "typescript": ">=5.3.3" }, "bin": { "errors": "bin/cli.mjs" } }, "sha512-3YEtvcMvtcnTl4HahqLt0VnaGVf7vVWOnt6/uPky5e0qV6BlxDSbGkbBzttNjxLXHognV0AQi3pjvrtfUnZmbg=="], + "@solana/wallet-adapter-base": ["@solana/wallet-adapter-base@0.9.27", "", { "dependencies": { "@solana/wallet-standard-features": "^1.3.0", "@wallet-standard/base": "^1.1.0", "@wallet-standard/features": "^1.1.0", "eventemitter3": "^5.0.1" }, "peerDependencies": { "@solana/web3.js": "^1.98.0" } }, "sha512-kXjeNfNFVs/NE9GPmysBRKQ/nf+foSaq3kfVSeMcO/iVgigyRmB551OjU3WyAolLG/1jeEfKLqF9fKwMCRkUqg=="], "@solana/wallet-adapter-coinbase": ["@solana/wallet-adapter-coinbase@0.1.23", "", { "dependencies": { "@solana/wallet-adapter-base": "^0.9.27" }, "peerDependencies": { "@solana/web3.js": "^1.98.0" } }, "sha512-vCJi/clbq1VVgydPFnHGAc2jdEhDAClYmhEAR4RJp9UHBg+MEQUl1WW8PVIREY5uOzJHma0qEiyummIfyt0b4A=="], @@ -3495,7 +3533,7 @@ "@types/express": ["@types/express@4.17.23", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "*" } }, "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ=="], - "@types/express-serve-static-core": ["@types/express-serve-static-core@5.1.0", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA=="], + "@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.7", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg=="], "@types/filesystem": ["@types/filesystem@0.0.36", "", { "dependencies": { "@types/filewriter": "*" } }, "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA=="], @@ -3689,7 +3727,7 @@ "@uniswap/biome-config": ["@uniswap/biome-config@workspace:packages/biome-config"], - "@uniswap/client-data-api": ["@uniswap/client-data-api@0.0.14", "", {}, "sha512-OpZmRP2YbeIfjZ9fDsnVWk7rFa0xwhByfjt1Ma+VJgObUGyi8ecCuczZ4ntrpNqFpij/kYUrm7i36gR2yGow/Q=="], + "@uniswap/client-data-api": ["@uniswap/client-data-api@0.0.18", "", {}, "sha512-gM7Y4EJNdDfMoMQ9F7Il/NqeUus4zEIDdE8kf2OZTYoGmoiGp2WLrG8D/sToprCpYu0klXbdet2lXGDydkTyew=="], "@uniswap/client-embeddedwallet": ["@uniswap/client-embeddedwallet@0.0.16", "", {}, "sha512-zxlx3E2X0kKAw10FKOGlbFpX4yq3KJv9SEipBJxZae5OZH8Ki8UO+FBX/Ke9JPQmU1WUoiJ7NOknjIIDOjVmpw=="], @@ -3697,8 +3735,6 @@ "@uniswap/client-platform-service": ["@uniswap/client-platform-service@0.0.5", "", {}, "sha512-vqxYuCRpddynuaF9+umgIEdo6EYFb+8VJvamjT6E1p1fx0MC0YATWfQvASzKkPS6oKjKzUBQz8nxCLT+v7aNNg=="], - "@uniswap/client-pools": ["@uniswap/client-pools@0.0.17", "", {}, "sha512-qOmKD3r2R9WjK3nHMmNvDDUPKGA/8SFxUNNZupnIK2oUdkGfKY1xadkbcA4iqN3F00511LQ21c5OZDgxxjelSw=="], - "@uniswap/client-search": ["@uniswap/client-search@0.0.10", "", {}, "sha512-ykHIxTR0dRtI3dK1ubHu2jNe+hfhDjyUfqA/dlNVrjM67GLYjF1Ls4kWi/cfR131XeRWj9gVsZv30CgzkOicAQ=="], "@uniswap/client-trading": ["@uniswap/client-trading@0.1.0", "", {}, "sha512-LWjbAUk3TFvWOlfbXweyME12EUmO6jEQPiCM9jaHA6pWtlWOWPbQcCXuDWy3iwHklupoqhnyiLGj8CFgvpl0lA=="], @@ -3751,6 +3787,10 @@ "@universe/config": ["@universe/config@workspace:packages/config"], + "@universe/gating": ["@universe/gating@workspace:packages/gating"], + + "@universe/notifications": ["@universe/notifications@workspace:packages/notifications"], + "@universe/sessions": ["@universe/sessions@workspace:packages/sessions"], "@universe/uniswap-nx": ["@universe/uniswap-nx@workspace:tools/uniswap-nx"], @@ -4147,7 +4187,7 @@ "ast-types-flow": ["ast-types-flow@0.0.8", "", {}, "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ=="], - "ast-v8-to-istanbul": ["ast-v8-to-istanbul@0.3.6", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", "js-tokens": "^9.0.1" } }, "sha512-9tx1z/7OF/a8EdYL3FKoBhxLf3h3D8fXvuSj0HknsVeli2HE40qbNZxyFhMtnydaRiamwFu9zhb+BsJ5tVPehQ=="], + "ast-v8-to-istanbul": ["ast-v8-to-istanbul@0.3.7", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", "js-tokens": "^9.0.1" } }, "sha512-kr1Hy6YRZBkGQSb6puP+D6FQ59Cx4m0siYhAxygMCAgadiWQ6oxAxQXHOMvJx67SJ63jRoVIIg5eXzUbbct1ww=="], "astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="], @@ -4265,7 +4305,7 @@ "base64-sol": ["base64-sol@1.0.1", "", {}, "sha512-ld3cCNMeXt4uJXmLZBHFGMvVpK9KsLVEhPpFRXnvSVAqABKbuNZg/+dsq3NuM+wxFLb/UrVkz7m1ciWmkMfTbg=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.8.16", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.8.17", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-j5zJcx6golJYTG6c05LUZ3Z8Gi+M62zRT/ycz4Xq4iCOdpcxwg7ngEYD4KA0eWZC7U17qh/Smq8bYbACJ0ipBA=="], "basic-auth": ["basic-auth@2.0.1", "", { "dependencies": { "safe-buffer": "5.1.2" } }, "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg=="], @@ -4401,7 +4441,7 @@ "bytes": ["bytes@3.0.0", "", {}, "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw=="], - "c12": ["c12@3.3.0", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^17.2.2", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.5.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", "pkg-types": "^2.3.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-K9ZkuyeJQeqLEyqldbYLG3wjqwpw4BVaAqvmxq3GYKK0b1A/yYQdIcJxkzAOWcNVWhJpRXAPfZFueekiY/L8Dw=="], + "c12": ["c12@3.3.1", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^17.2.3", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", "pkg-types": "^2.3.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-LcWQ01LT9tkoUINHgpIOv3mMs+Abv7oVCrtpMRi1PaapVEpWoMga5WuT7/DqFTu7URP9ftbOmimNw1KNIGh9DQ=="], "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], @@ -4439,7 +4479,7 @@ "caniuse-api": ["caniuse-api@3.0.0", "", { "dependencies": { "browserslist": "^4.0.0", "caniuse-lite": "^1.0.0", "lodash.memoize": "^4.1.2", "lodash.uniq": "^4.5.0" } }, "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw=="], - "caniuse-lite": ["caniuse-lite@1.0.30001750", "", {}, "sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ=="], + "caniuse-lite": ["caniuse-lite@1.0.30001751", "", {}, "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw=="], "canvaskit-wasm": ["canvaskit-wasm@0.40.0", "", { "dependencies": { "@webgpu/types": "0.1.21" } }, "sha512-Od2o+ZmoEw9PBdN/yCGvzfu0WVqlufBPEWNG452wY7E9aT8RBE+ChpZF526doOlg7zumO4iCS+RAeht4P0Gbpw=="], @@ -5079,7 +5119,7 @@ "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], - "envinfo": ["envinfo@7.18.0", "", { "bin": { "envinfo": "dist/cli.js" } }, "sha512-02QGCLRW+Jb8PC270ic02lat+N57iBaWsvHjcJViqp6UVupRB+Vsg7brYPTqEFXvsdTql3KnSczv5ModZFpl8Q=="], + "envinfo": ["envinfo@7.19.0", "", { "bin": { "envinfo": "dist/cli.js" } }, "sha512-DoSM9VyG6O3vqBf+p3Gjgr/Q52HYBBtO3v+4koAxt1MnWr+zEnxE+nke/yXS4lt2P4SYCHQ4V3f1i88LQVOpAw=="], "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], @@ -5381,6 +5421,8 @@ "fastest-levenshtein": ["fastest-levenshtein@1.0.16", "", {}, "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg=="], + "fastestsmallesttextencoderdecoder": ["fastestsmallesttextencoderdecoder@1.0.22", "", {}, "sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw=="], + "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], "faye-websocket": ["faye-websocket@0.11.4", "", { "dependencies": { "websocket-driver": ">=0.5.1" } }, "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g=="], @@ -6683,7 +6725,7 @@ "node-preload": ["node-preload@0.2.1", "", { "dependencies": { "process-on-spawn": "^1.0.0" } }, "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ=="], - "node-releases": ["node-releases@2.0.23", "", {}, "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg=="], + "node-releases": ["node-releases@2.0.25", "", {}, "sha512-4auku8B/vw5psvTiiN9j1dAOsXvMoGqJuKJcR+dTdqiXEK20mMTk1UEo3HS16LeGQsVG6+qKTPM9u/qQ2LqATA=="], "node-source-walk": ["node-source-walk@5.0.2", "", { "dependencies": { "@babel/parser": "^7.21.4" } }, "sha512-Y4jr/8SRS5hzEdZ7SGuvZGwfORvNsSsNRwDXx5WisiqzsVfeftDvRgfeqWNgZvWSJbgubTRVRYBzK6UO+ErqjA=="], @@ -8849,8 +8891,6 @@ "@babel/runtime/regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="], - "@binance/w3w-qrcode-modal/qrcode": ["qrcode@1.5.4", "", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="], - "@chromatic-com/storybook/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], "@cloudflare/kv-asset-handler/mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], @@ -8865,7 +8905,7 @@ "@commitlint/load/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "@commitlint/load/cosmiconfig": ["cosmiconfig@8.0.0", "", { "dependencies": { "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "parse-json": "^5.0.0", "path-type": "^4.0.0" } }, "sha512-da1EafcpH6b/TD8vDRaWV7xFINlHlF6zKsGwS1TsuVJTZRkquaS5HTMq7uq6h31619QjbsYl21gVDOm32KM1vQ=="], + "@commitlint/load/cosmiconfig": ["cosmiconfig@8.3.6", "", { "dependencies": { "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0", "path-type": "^4.0.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA=="], "@commitlint/read/fs-extra": ["fs-extra@11.3.2", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A=="], @@ -8949,26 +8989,8 @@ "@ethersproject/basex/@ethersproject/properties": ["@ethersproject/properties@5.8.0", "", { "dependencies": { "@ethersproject/logger": "^5.8.0" } }, "sha512-PYuiEoQ+FMaZZNGrStmN7+lWjlsoufGIHdww7454FIaGdbe/p5rnaCXTr5MtBYl3NkeoVhHZuyzChPeGeKIpQw=="], - "@ethersproject/hdnode/@ethersproject/bignumber": ["@ethersproject/bignumber@5.8.0", "", { "dependencies": { "@ethersproject/bytes": "^5.8.0", "@ethersproject/logger": "^5.8.0", "bn.js": "^5.2.1" } }, "sha512-ZyaT24bHaSeJon2tGPKIiHszWjD/54Sz8t57Toch475lCLljC6MgPmxk7Gtzz+ddNN5LuHea9qhAe0x3D+uYPA=="], - - "@ethersproject/hdnode/@ethersproject/properties": ["@ethersproject/properties@5.8.0", "", { "dependencies": { "@ethersproject/logger": "^5.8.0" } }, "sha512-PYuiEoQ+FMaZZNGrStmN7+lWjlsoufGIHdww7454FIaGdbe/p5rnaCXTr5MtBYl3NkeoVhHZuyzChPeGeKIpQw=="], - - "@ethersproject/hdnode/@ethersproject/signing-key": ["@ethersproject/signing-key@5.8.0", "", { "dependencies": { "@ethersproject/bytes": "^5.8.0", "@ethersproject/logger": "^5.8.0", "@ethersproject/properties": "^5.8.0", "bn.js": "^5.2.1", "elliptic": "6.6.1", "hash.js": "1.1.7" } }, "sha512-LrPW2ZxoigFi6U6aVkFN/fa9Yx/+4AtIUe4/HACTvKJdhm0eeb107EVCIQcrLZkxaSIgc/eCrX8Q1GtbH+9n3w=="], - - "@ethersproject/hdnode/@ethersproject/strings": ["@ethersproject/strings@5.8.0", "", { "dependencies": { "@ethersproject/bytes": "^5.8.0", "@ethersproject/constants": "^5.8.0", "@ethersproject/logger": "^5.8.0" } }, "sha512-qWEAk0MAvl0LszjdfnZ2uC8xbR2wdv4cDabyHiBh3Cldq/T8dPH3V4BbBsAYJUeonwD+8afVXld274Ls+Y1xXg=="], - - "@ethersproject/json-wallets/@ethersproject/address": ["@ethersproject/address@5.8.0", "", { "dependencies": { "@ethersproject/bignumber": "^5.8.0", "@ethersproject/bytes": "^5.8.0", "@ethersproject/keccak256": "^5.8.0", "@ethersproject/logger": "^5.8.0", "@ethersproject/rlp": "^5.8.0" } }, "sha512-GhH/abcC46LJwshoN+uBNoKVFPxUuZm6dA257z0vZkKmU1+t8xTn8oK7B9qrj8W2rFRMch4gbJl6PmVxjxBEBA=="], - - "@ethersproject/json-wallets/@ethersproject/keccak256": ["@ethersproject/keccak256@5.8.0", "", { "dependencies": { "@ethersproject/bytes": "^5.8.0", "js-sha3": "0.8.0" } }, "sha512-A1pkKLZSz8pDaQ1ftutZoaN46I6+jvuqugx5KYNeQOPqq+JZ0Txm7dlWesCHB5cndJSu5vP2VKptKf7cksERng=="], - - "@ethersproject/json-wallets/@ethersproject/properties": ["@ethersproject/properties@5.8.0", "", { "dependencies": { "@ethersproject/logger": "^5.8.0" } }, "sha512-PYuiEoQ+FMaZZNGrStmN7+lWjlsoufGIHdww7454FIaGdbe/p5rnaCXTr5MtBYl3NkeoVhHZuyzChPeGeKIpQw=="], - - "@ethersproject/json-wallets/@ethersproject/strings": ["@ethersproject/strings@5.8.0", "", { "dependencies": { "@ethersproject/bytes": "^5.8.0", "@ethersproject/constants": "^5.8.0", "@ethersproject/logger": "^5.8.0" } }, "sha512-qWEAk0MAvl0LszjdfnZ2uC8xbR2wdv4cDabyHiBh3Cldq/T8dPH3V4BbBsAYJUeonwD+8afVXld274Ls+Y1xXg=="], - "@ethersproject/providers/ws": ["ws@7.4.6", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A=="], - "@ethersproject/signing-key/@ethersproject/properties": ["@ethersproject/properties@5.8.0", "", { "dependencies": { "@ethersproject/logger": "^5.8.0" } }, "sha512-PYuiEoQ+FMaZZNGrStmN7+lWjlsoufGIHdww7454FIaGdbe/p5rnaCXTr5MtBYl3NkeoVhHZuyzChPeGeKIpQw=="], - "@ethersproject/solidity/@ethersproject/bignumber": ["@ethersproject/bignumber@5.8.0", "", { "dependencies": { "@ethersproject/bytes": "^5.8.0", "@ethersproject/logger": "^5.8.0", "bn.js": "^5.2.1" } }, "sha512-ZyaT24bHaSeJon2tGPKIiHszWjD/54Sz8t57Toch475lCLljC6MgPmxk7Gtzz+ddNN5LuHea9qhAe0x3D+uYPA=="], "@ethersproject/solidity/@ethersproject/keccak256": ["@ethersproject/keccak256@5.8.0", "", { "dependencies": { "@ethersproject/bytes": "^5.8.0", "js-sha3": "0.8.0" } }, "sha512-A1pkKLZSz8pDaQ1ftutZoaN46I6+jvuqugx5KYNeQOPqq+JZ0Txm7dlWesCHB5cndJSu5vP2VKptKf7cksERng=="], @@ -8987,20 +9009,8 @@ "@ethersproject/transactions/@ethersproject/signing-key": ["@ethersproject/signing-key@5.8.0", "", { "dependencies": { "@ethersproject/bytes": "^5.8.0", "@ethersproject/logger": "^5.8.0", "@ethersproject/properties": "^5.8.0", "bn.js": "^5.2.1", "elliptic": "6.6.1", "hash.js": "1.1.7" } }, "sha512-LrPW2ZxoigFi6U6aVkFN/fa9Yx/+4AtIUe4/HACTvKJdhm0eeb107EVCIQcrLZkxaSIgc/eCrX8Q1GtbH+9n3w=="], - "@ethersproject/wallet/@ethersproject/address": ["@ethersproject/address@5.8.0", "", { "dependencies": { "@ethersproject/bignumber": "^5.8.0", "@ethersproject/bytes": "^5.8.0", "@ethersproject/keccak256": "^5.8.0", "@ethersproject/logger": "^5.8.0", "@ethersproject/rlp": "^5.8.0" } }, "sha512-GhH/abcC46LJwshoN+uBNoKVFPxUuZm6dA257z0vZkKmU1+t8xTn8oK7B9qrj8W2rFRMch4gbJl6PmVxjxBEBA=="], - - "@ethersproject/wallet/@ethersproject/bignumber": ["@ethersproject/bignumber@5.8.0", "", { "dependencies": { "@ethersproject/bytes": "^5.8.0", "@ethersproject/logger": "^5.8.0", "bn.js": "^5.2.1" } }, "sha512-ZyaT24bHaSeJon2tGPKIiHszWjD/54Sz8t57Toch475lCLljC6MgPmxk7Gtzz+ddNN5LuHea9qhAe0x3D+uYPA=="], - - "@ethersproject/wallet/@ethersproject/keccak256": ["@ethersproject/keccak256@5.8.0", "", { "dependencies": { "@ethersproject/bytes": "^5.8.0", "js-sha3": "0.8.0" } }, "sha512-A1pkKLZSz8pDaQ1ftutZoaN46I6+jvuqugx5KYNeQOPqq+JZ0Txm7dlWesCHB5cndJSu5vP2VKptKf7cksERng=="], - - "@ethersproject/wallet/@ethersproject/properties": ["@ethersproject/properties@5.8.0", "", { "dependencies": { "@ethersproject/logger": "^5.8.0" } }, "sha512-PYuiEoQ+FMaZZNGrStmN7+lWjlsoufGIHdww7454FIaGdbe/p5rnaCXTr5MtBYl3NkeoVhHZuyzChPeGeKIpQw=="], - - "@ethersproject/wallet/@ethersproject/signing-key": ["@ethersproject/signing-key@5.8.0", "", { "dependencies": { "@ethersproject/bytes": "^5.8.0", "@ethersproject/logger": "^5.8.0", "@ethersproject/properties": "^5.8.0", "bn.js": "^5.2.1", "elliptic": "6.6.1", "hash.js": "1.1.7" } }, "sha512-LrPW2ZxoigFi6U6aVkFN/fa9Yx/+4AtIUe4/HACTvKJdhm0eeb107EVCIQcrLZkxaSIgc/eCrX8Q1GtbH+9n3w=="], - "@expo/bunyan/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], - "@expo/cli/@babel/runtime": ["@babel/runtime@7.28.2", "", {}, "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA=="], - "@expo/cli/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "@expo/cli/ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], @@ -9019,6 +9029,8 @@ "@expo/cli/picomatch": ["picomatch@3.0.1", "", {}, "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag=="], + "@expo/cli/send": ["send@0.19.1", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg=="], + "@expo/cli/source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], "@expo/cli/tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="], @@ -9267,8 +9279,6 @@ "@graphql-tools/executor/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@graphql-tools/executor-graphql-ws/@repeaterjs/repeater": ["@repeaterjs/repeater@3.0.4", "", {}, "sha512-AW8PKd6iX3vAZ0vA43nOUOnbq/X5ihgU+mSXXqunMkeQADGiqw/PY0JNeYtD5sr0PAy51YPgAPbDoeapv9r8WA=="], - "@graphql-tools/executor-graphql-ws/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "@graphql-tools/executor-graphql-ws/ws": ["ws@8.13.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA=="], @@ -9473,8 +9483,6 @@ "@metamask/providers/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - "@metamask/sdk/@babel/runtime": ["@babel/runtime@7.28.2", "", {}, "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA=="], - "@metamask/sdk/cross-fetch": ["cross-fetch@4.1.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw=="], "@metamask/sdk/pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], @@ -9505,26 +9513,12 @@ "@nicolo-ribaudo/eslint-scope-5-internals/eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="], - "@nx/devkit/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@nx/eslint/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@nx/jest/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@nx/js/@babel/core": ["@babel/core@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA=="], - - "@nx/js/@babel/runtime": ["@babel/runtime@7.28.2", "", {}, "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA=="], - "@nx/js/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "@nx/js/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@nx/plugin/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@nx/workspace/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "@nx/workspace/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@octokit/endpoint/is-plain-object": ["is-plain-object@5.0.0", "", {}, "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="], "@octokit/request/is-plain-object": ["is-plain-object@5.0.0", "", {}, "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="], @@ -9807,6 +9801,12 @@ "@solana-mobile/wallet-standard-mobile/qrcode": ["qrcode@1.5.4", "", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="], + "@solana/errors/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "@solana/errors/commander": ["commander@14.0.1", "", {}, "sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A=="], + + "@solana/wallet-standard-util/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], + "@solana/web3.js/bs58": ["bs58@4.0.1", "", { "dependencies": { "base-x": "^3.0.2" } }, "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw=="], "@solana/web3.js/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], @@ -9815,8 +9815,6 @@ "@storybook/addon-actions/polished": ["polished@4.3.1", "", { "dependencies": { "@babel/runtime": "^7.17.8" } }, "sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA=="], - "@storybook/addon-actions/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], - "@storybook/addon-essentials/@storybook/addon-controls": ["@storybook/addon-controls@8.5.2", "", { "dependencies": { "@storybook/global": "^5.0.0", "dequal": "^2.0.2", "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^8.5.2" } }, "sha512-wkzw2vRff4zkzdvC/GOlB2PlV0i973u8igSLeg34TWNEAa4bipwVHnFfIojRuP9eN1bZL/0tjuU5pKnbTqH7aQ=="], "@storybook/addon-interactions/polished": ["polished@4.3.1", "", { "dependencies": { "@babel/runtime": "^7.17.8" } }, "sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA=="], @@ -9865,8 +9863,6 @@ "@storybook/test/@vitest/spy": ["@vitest/spy@2.0.5", "", { "dependencies": { "tinyspy": "^3.0.0" } }, "sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA=="], - "@storybook/test-runner/@babel/core": ["@babel/core@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA=="], - "@storybook/test-runner/@storybook/csf": ["@storybook/csf@0.1.13", "", { "dependencies": { "type-fest": "^2.19.0" } }, "sha512-7xOOwCLGB3ebM87eemep89MYRFTko+D8qE7EdAAq74lgdqRR5cOUtYWJLjO2dLtP94nqoOdHJo6MdLLKzg412Q=="], "@surma/rollup-plugin-off-main-thread/magic-string": ["magic-string@0.25.9", "", { "dependencies": { "sourcemap-codec": "^1.4.8" } }, "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ=="], @@ -9903,8 +9899,6 @@ "@tamagui/static/@babel/core": ["@babel/core@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA=="], - "@tamagui/static/@babel/runtime": ["@babel/runtime@7.28.2", "", {}, "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA=="], - "@tamagui/static/esbuild": ["esbuild@0.24.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.24.2", "@esbuild/android-arm": "0.24.2", "@esbuild/android-arm64": "0.24.2", "@esbuild/android-x64": "0.24.2", "@esbuild/darwin-arm64": "0.24.2", "@esbuild/darwin-x64": "0.24.2", "@esbuild/freebsd-arm64": "0.24.2", "@esbuild/freebsd-x64": "0.24.2", "@esbuild/linux-arm": "0.24.2", "@esbuild/linux-arm64": "0.24.2", "@esbuild/linux-ia32": "0.24.2", "@esbuild/linux-loong64": "0.24.2", "@esbuild/linux-mips64el": "0.24.2", "@esbuild/linux-ppc64": "0.24.2", "@esbuild/linux-riscv64": "0.24.2", "@esbuild/linux-s390x": "0.24.2", "@esbuild/linux-x64": "0.24.2", "@esbuild/netbsd-arm64": "0.24.2", "@esbuild/netbsd-x64": "0.24.2", "@esbuild/openbsd-arm64": "0.24.2", "@esbuild/openbsd-x64": "0.24.2", "@esbuild/sunos-x64": "0.24.2", "@esbuild/win32-arm64": "0.24.2", "@esbuild/win32-ia32": "0.24.2", "@esbuild/win32-x64": "0.24.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA=="], "@tamagui/static/fs-extra": ["fs-extra@11.3.2", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A=="], @@ -9937,8 +9931,6 @@ "@tybys/wasm-util/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@types/express/@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.7", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg=="], - "@types/request/form-data": ["form-data@2.5.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.35", "safe-buffer": "^5.2.1" } }, "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A=="], "@typescript-eslint/typescript-estree/globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], @@ -10833,8 +10825,6 @@ "jest-util/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "jest-util/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - "jest-validate/@jest/types": ["@jest/types@30.2.0", "", { "dependencies": { "@jest/pattern": "30.0.1", "@jest/schemas": "30.0.5", "@types/istanbul-lib-coverage": "^2.0.6", "@types/istanbul-reports": "^3.0.4", "@types/node": "*", "@types/yargs": "^17.0.33", "chalk": "^4.1.2" } }, "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg=="], "jest-validate/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -10881,10 +10871,6 @@ "keccak/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - "knip/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - - "knip/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "latest-version/package-json": ["package-json@10.0.1", "", { "dependencies": { "ky": "^1.2.0", "registry-auth-token": "^5.0.2", "registry-url": "^6.0.1", "semver": "^7.6.0" } }, "sha512-ua1L4OgXSBdsu1FPb7F3tYH0F48a6kxvod4pLUlGY9COeJAJQNX/sNH2IiEmsxw7lqYiAwrdHMjz1FctOsyDQg=="], "lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], @@ -10933,8 +10919,6 @@ "matcher-collection/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - "md5.js/hash-base": ["hash-base@3.1.2", "", { "dependencies": { "inherits": "^2.0.4", "readable-stream": "^2.3.8", "safe-buffer": "^5.2.1", "to-buffer": "^1.2.1" } }, "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg=="], - "meow/type-fest": ["type-fest@0.18.1", "", {}, "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw=="], "meow/yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="], @@ -10973,10 +10957,6 @@ "metro-file-map/jest-worker": ["jest-worker@29.7.0", "", { "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw=="], - "metro-minify-terser/terser": ["terser@5.44.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w=="], - - "metro-runtime/@babel/runtime": ["@babel/runtime@7.28.2", "", {}, "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA=="], - "metro-source-map/source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="], "metro-source-map/vlq": ["vlq@1.0.1", "", {}, "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w=="], @@ -11577,8 +11557,6 @@ "tamagui-loader/esbuild-loader": ["esbuild-loader@4.4.0", "", { "dependencies": { "esbuild": "^0.25.0", "get-tsconfig": "^4.10.1", "loader-utils": "^2.0.4", "webpack-sources": "^1.4.3" }, "peerDependencies": { "webpack": "^4.40.0 || ^5.0.0" } }, "sha512-4J+hXTpTtEdzUNLoY8ReqDNJx2NoldfiljRCiKbeYUuZmVaiJeDqFgyAzz8uOopaekwRoCcqBFyEroGQLFVZ1g=="], - "tamagui-loader/fs-extra": ["fs-extra@11.3.2", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A=="], - "tamagui-loader/loader-utils": ["loader-utils@3.3.1", "", {}, "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg=="], "tar/mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], @@ -11695,18 +11673,12 @@ "vinyl-sourcemap/vinyl": ["vinyl@2.2.1", "", { "dependencies": { "clone": "^2.1.1", "clone-buffer": "^1.0.0", "clone-stats": "^1.0.0", "cloneable-readable": "^1.0.0", "remove-trailing-separator": "^1.0.1", "replace-ext": "^1.0.0" } }, "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw=="], - "vite/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - - "vite-plugin-bundlesize/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - "vite-plugin-bundlesize/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], "vite-plugin-svgr/@svgr/core": ["@svgr/core@8.1.0", "", { "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", "camelcase": "^6.2.0", "cosmiconfig": "^8.1.3", "snake-case": "^3.0.4" } }, "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA=="], "vite-plugin-svgr/@svgr/plugin-jsx": ["@svgr/plugin-jsx@8.1.0", "", { "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", "@svgr/hast-util-to-babel-ast": "8.0.0", "svg-parser": "^2.0.4" }, "peerDependencies": { "@svgr/core": "*" } }, "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA=="], - "vitest/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - "vitest/vite-node": ["vite-node@3.2.1", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-V4EyKQPxquurNJPtQJRZo8hKOoKNBRIhxcDbQFPFig0JdoWcUhwRgK8yoCXXrfYVPKS6XwirGHPszLnR8FbjCA=="], "wagmi/use-sync-external-store": ["use-sync-external-store@1.4.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw=="], @@ -11839,8 +11811,6 @@ "@babel/register/source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - "@binance/w3w-qrcode-modal/qrcode/yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="], - "@chromatic-com/storybook/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], "@commitlint/format/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -11877,12 +11847,6 @@ "@ethersproject/abstract-provider/@ethersproject/web/@ethersproject/strings": ["@ethersproject/strings@5.8.0", "", { "dependencies": { "@ethersproject/bytes": "^5.8.0", "@ethersproject/constants": "^5.8.0", "@ethersproject/logger": "^5.8.0" } }, "sha512-qWEAk0MAvl0LszjdfnZ2uC8xbR2wdv4cDabyHiBh3Cldq/T8dPH3V4BbBsAYJUeonwD+8afVXld274Ls+Y1xXg=="], - "@ethersproject/hdnode/@ethersproject/strings/@ethersproject/constants": ["@ethersproject/constants@5.8.0", "", { "dependencies": { "@ethersproject/bignumber": "^5.8.0" } }, "sha512-wigX4lrf5Vu+axVTIvNsuL6YrV4O5AXl5ubcURKMEME5TnWBouUh0CDTWxZ2GpnRn1kcCgE7l8O5+VbV9QTTcg=="], - - "@ethersproject/json-wallets/@ethersproject/address/@ethersproject/bignumber": ["@ethersproject/bignumber@5.8.0", "", { "dependencies": { "@ethersproject/bytes": "^5.8.0", "@ethersproject/logger": "^5.8.0", "bn.js": "^5.2.1" } }, "sha512-ZyaT24bHaSeJon2tGPKIiHszWjD/54Sz8t57Toch475lCLljC6MgPmxk7Gtzz+ddNN5LuHea9qhAe0x3D+uYPA=="], - - "@ethersproject/json-wallets/@ethersproject/strings/@ethersproject/constants": ["@ethersproject/constants@5.8.0", "", { "dependencies": { "@ethersproject/bignumber": "^5.8.0" } }, "sha512-wigX4lrf5Vu+axVTIvNsuL6YrV4O5AXl5ubcURKMEME5TnWBouUh0CDTWxZ2GpnRn1kcCgE7l8O5+VbV9QTTcg=="], - "@ethersproject/solidity/@ethersproject/strings/@ethersproject/constants": ["@ethersproject/constants@5.8.0", "", { "dependencies": { "@ethersproject/bignumber": "^5.8.0" } }, "sha512-wigX4lrf5Vu+axVTIvNsuL6YrV4O5AXl5ubcURKMEME5TnWBouUh0CDTWxZ2GpnRn1kcCgE7l8O5+VbV9QTTcg=="], "@expo/cli/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -11911,6 +11875,10 @@ "@expo/cli/ora/strip-ansi": ["strip-ansi@5.2.0", "", { "dependencies": { "ansi-regex": "^4.1.0" } }, "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA=="], + "@expo/cli/send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "@expo/cli/send/range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + "@expo/cli/source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "@expo/cli/tar/chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="], @@ -12361,6 +12329,10 @@ "@solana/web3.js/node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + "@storybook/addon-actions/polished/@babel/runtime": ["@babel/runtime@7.28.2", "", {}, "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA=="], + + "@storybook/addon-interactions/polished/@babel/runtime": ["@babel/runtime@7.28.2", "", {}, "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA=="], + "@storybook/builder-webpack5/terser-webpack-plugin/schema-utils": ["schema-utils@4.3.3", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA=="], "@storybook/builder-webpack5/terser-webpack-plugin/terser": ["terser@5.44.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w=="], @@ -12375,6 +12347,8 @@ "@storybook/preset-react-webpack/find-up/locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + "@storybook/react-native-theming/polished/@babel/runtime": ["@babel/runtime@7.28.2", "", {}, "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA=="], + "@storybook/react-native-ui/@storybook/react/@storybook/components": ["@storybook/components@8.6.14", "", { "peerDependencies": { "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" } }, "sha512-HNR2mC5I4Z5ek8kTrVZlIY/B8gJGs5b3XdZPBPBopTIN6U/YHXiDyOjY3JlaS4fSG1fVhp/Qp1TpMn1w/9m1pw=="], "@storybook/react-native-ui/@storybook/react/@storybook/manager-api": ["@storybook/manager-api@8.6.14", "", { "peerDependencies": { "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" } }, "sha512-ez0Zihuy17udLbfHZQXkGqwtep0mSGgHcNzGN7iZrMP1m+VmNo+7aGCJJdvXi7+iU3yq8weXSQFWg5DqWgLS7g=="], @@ -12385,6 +12359,8 @@ "@storybook/react-native-ui/@storybook/react/@storybook/theming": ["@storybook/theming@8.6.14", "", { "peerDependencies": { "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" } }, "sha512-r4y+LsiB37V5hzpQo+BM10PaCsp7YlZ0YcZzQP1OCkPlYXmUAFy2VvDKaFRpD8IeNPKug2u4iFm/laDEbs03dg=="], + "@storybook/react-native-ui/polished/@babel/runtime": ["@babel/runtime@7.28.2", "", {}, "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA=="], + "@storybook/react-native/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "@storybook/test/@testing-library/jest-dom/chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="], @@ -13461,8 +13437,6 @@ "matcher-collection/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - "md5.js/hash-base/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], - "metro-config/cosmiconfig/import-fresh": ["import-fresh@2.0.0", "", { "dependencies": { "caller-path": "^2.0.0", "resolve-from": "^3.0.0" } }, "sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg=="], "metro-config/cosmiconfig/js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="], @@ -13471,14 +13445,10 @@ "metro-config/jest-validate/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "metro-config/metro-runtime/@babel/runtime": ["@babel/runtime@7.28.2", "", {}, "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA=="], - "metro-file-map/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "metro-file-map/jest-worker/jest-util": ["jest-util@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", "graceful-fs": "^4.2.9", "picomatch": "^2.2.3" } }, "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA=="], - "metro-minify-terser/terser/source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], - "metro-transform-worker/metro-source-map/metro-symbolicate": ["metro-symbolicate@0.81.1", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.81.1", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-Lgk0qjEigtFtsM7C0miXITbcV47E1ZYIfB+m/hCraihiwRWkNUQEPCWvqZmwXKSwVE5mXA0EzQtghAvQSjZDxw=="], "metro-transform-worker/metro-source-map/ob1": ["ob1@0.81.1", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-1PEbvI+AFvOcgdNcO79FtDI1TUO8S3lhiKOyAiyWQF3sFDDKS+aw2/BZvGlArFnSmqckwOOB9chQuIX0/OahoQ=="], @@ -13497,8 +13467,6 @@ "metro/jest-worker/jest-util": ["jest-util@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", "graceful-fs": "^4.2.9", "picomatch": "^2.2.3" } }, "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA=="], - "metro/metro-runtime/@babel/runtime": ["@babel/runtime@7.28.2", "", {}, "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA=="], - "metro/metro-source-map/ob1": ["ob1@0.81.1", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-1PEbvI+AFvOcgdNcO79FtDI1TUO8S3lhiKOyAiyWQF3sFDDKS+aw2/BZvGlArFnSmqckwOOB9chQuIX0/OahoQ=="], "metro/metro-source-map/vlq": ["vlq@1.0.1", "", {}, "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w=="], @@ -13827,7 +13795,7 @@ "storybook/@storybook/core/@storybook/csf": ["@storybook/csf@0.1.12", "", { "dependencies": { "type-fest": "^2.19.0" } }, "sha512-9/exVhabisyIVL0VxTCxo01Tdm8wefIXKXfltAPTSr8cbLn5JAxGQ6QV3mjdecLGEOucfoVhAKtJfVHxEK1iqw=="], - "storybook/@storybook/core/esbuild": ["esbuild@0.19.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.19.12", "@esbuild/android-arm": "0.19.12", "@esbuild/android-arm64": "0.19.12", "@esbuild/android-x64": "0.19.12", "@esbuild/darwin-arm64": "0.19.12", "@esbuild/darwin-x64": "0.19.12", "@esbuild/freebsd-arm64": "0.19.12", "@esbuild/freebsd-x64": "0.19.12", "@esbuild/linux-arm": "0.19.12", "@esbuild/linux-arm64": "0.19.12", "@esbuild/linux-ia32": "0.19.12", "@esbuild/linux-loong64": "0.19.12", "@esbuild/linux-mips64el": "0.19.12", "@esbuild/linux-ppc64": "0.19.12", "@esbuild/linux-riscv64": "0.19.12", "@esbuild/linux-s390x": "0.19.12", "@esbuild/linux-x64": "0.19.12", "@esbuild/netbsd-x64": "0.19.12", "@esbuild/openbsd-x64": "0.19.12", "@esbuild/sunos-x64": "0.19.12", "@esbuild/win32-arm64": "0.19.12", "@esbuild/win32-ia32": "0.19.12", "@esbuild/win32-x64": "0.19.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg=="], + "storybook/@storybook/core/esbuild": ["esbuild@0.24.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.24.2", "@esbuild/android-arm": "0.24.2", "@esbuild/android-arm64": "0.24.2", "@esbuild/android-x64": "0.24.2", "@esbuild/darwin-arm64": "0.24.2", "@esbuild/darwin-x64": "0.24.2", "@esbuild/freebsd-arm64": "0.24.2", "@esbuild/freebsd-x64": "0.24.2", "@esbuild/linux-arm": "0.24.2", "@esbuild/linux-arm64": "0.24.2", "@esbuild/linux-ia32": "0.24.2", "@esbuild/linux-loong64": "0.24.2", "@esbuild/linux-mips64el": "0.24.2", "@esbuild/linux-ppc64": "0.24.2", "@esbuild/linux-riscv64": "0.24.2", "@esbuild/linux-s390x": "0.24.2", "@esbuild/linux-x64": "0.24.2", "@esbuild/netbsd-arm64": "0.24.2", "@esbuild/netbsd-x64": "0.24.2", "@esbuild/openbsd-arm64": "0.24.2", "@esbuild/openbsd-x64": "0.24.2", "@esbuild/sunos-x64": "0.24.2", "@esbuild/win32-arm64": "0.24.2", "@esbuild/win32-ia32": "0.24.2", "@esbuild/win32-x64": "0.24.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA=="], "styled-components/@emotion/is-prop-valid/@emotion/memoize": ["@emotion/memoize@0.9.0", "", {}, "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ=="], @@ -13893,6 +13861,8 @@ "unstorage/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + "update-check/registry-auth-token/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + "update-notifier/boxen/camelcase": ["camelcase@8.0.0", "", {}, "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA=="], "update-notifier/boxen/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], @@ -14055,12 +14025,6 @@ "@babel/register/find-cache-dir/pkg-dir/find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="], - "@binance/w3w-qrcode-modal/qrcode/yargs/cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="], - - "@binance/w3w-qrcode-modal/qrcode/yargs/y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="], - - "@binance/w3w-qrcode-modal/qrcode/yargs/yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="], - "@commitlint/format/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], "@commitlint/load/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], @@ -14079,8 +14043,6 @@ "@ethersproject/abstract-provider/@ethersproject/web/@ethersproject/strings/@ethersproject/constants": ["@ethersproject/constants@5.8.0", "", { "dependencies": { "@ethersproject/bignumber": "^5.8.0" } }, "sha512-wigX4lrf5Vu+axVTIvNsuL6YrV4O5AXl5ubcURKMEME5TnWBouUh0CDTWxZ2GpnRn1kcCgE7l8O5+VbV9QTTcg=="], - "@ethersproject/json-wallets/@ethersproject/strings/@ethersproject/constants/@ethersproject/bignumber": ["@ethersproject/bignumber@5.8.0", "", { "dependencies": { "@ethersproject/bytes": "^5.8.0", "@ethersproject/logger": "^5.8.0", "bn.js": "^5.2.1" } }, "sha512-ZyaT24bHaSeJon2tGPKIiHszWjD/54Sz8t57Toch475lCLljC6MgPmxk7Gtzz+ddNN5LuHea9qhAe0x3D+uYPA=="], - "@expo/cli/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], "@expo/cli/glob/foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], @@ -14093,6 +14055,8 @@ "@expo/cli/ora/strip-ansi/ansi-regex": ["ansi-regex@4.1.1", "", {}, "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g=="], + "@expo/cli/send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "@expo/cli/tar/fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], "@expo/cli/tar/minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], @@ -14711,10 +14675,6 @@ "madge/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - "md5.js/hash-base/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], - - "md5.js/hash-base/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], - "metro-config/cosmiconfig/import-fresh/resolve-from": ["resolve-from@3.0.0", "", {}, "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw=="], "metro-config/cosmiconfig/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], @@ -14729,8 +14689,6 @@ "metro-file-map/jest-worker/jest-util/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "metro-minify-terser/terser/source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - "metro/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], "metro/jest-worker/jest-util/ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], @@ -14947,51 +14905,55 @@ "static-eval/escodegen/optionator/type-check": ["type-check@0.3.2", "", { "dependencies": { "prelude-ls": "~1.1.2" } }, "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg=="], - "storybook/@storybook/core/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.19.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="], + "storybook/@storybook/core/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.24.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA=="], - "storybook/@storybook/core/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.19.12", "", { "os": "android", "cpu": "arm" }, "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w=="], + "storybook/@storybook/core/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.24.2", "", { "os": "android", "cpu": "arm" }, "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q=="], - "storybook/@storybook/core/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.19.12", "", { "os": "android", "cpu": "arm64" }, "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA=="], + "storybook/@storybook/core/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.24.2", "", { "os": "android", "cpu": "arm64" }, "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg=="], - "storybook/@storybook/core/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.19.12", "", { "os": "android", "cpu": "x64" }, "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew=="], + "storybook/@storybook/core/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.24.2", "", { "os": "android", "cpu": "x64" }, "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw=="], - "storybook/@storybook/core/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.19.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g=="], + "storybook/@storybook/core/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.24.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA=="], - "storybook/@storybook/core/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.19.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A=="], + "storybook/@storybook/core/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.24.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA=="], - "storybook/@storybook/core/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.19.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA=="], + "storybook/@storybook/core/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.24.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg=="], - "storybook/@storybook/core/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.19.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg=="], + "storybook/@storybook/core/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.24.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q=="], - "storybook/@storybook/core/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.19.12", "", { "os": "linux", "cpu": "arm" }, "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w=="], + "storybook/@storybook/core/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.24.2", "", { "os": "linux", "cpu": "arm" }, "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA=="], - "storybook/@storybook/core/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.19.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA=="], + "storybook/@storybook/core/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.24.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg=="], - "storybook/@storybook/core/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.19.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA=="], + "storybook/@storybook/core/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.24.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw=="], - "storybook/@storybook/core/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA=="], + "storybook/@storybook/core/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.24.2", "", { "os": "linux", "cpu": "none" }, "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ=="], - "storybook/@storybook/core/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w=="], + "storybook/@storybook/core/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.24.2", "", { "os": "linux", "cpu": "none" }, "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw=="], - "storybook/@storybook/core/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.19.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg=="], + "storybook/@storybook/core/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.24.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw=="], - "storybook/@storybook/core/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg=="], + "storybook/@storybook/core/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.24.2", "", { "os": "linux", "cpu": "none" }, "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q=="], - "storybook/@storybook/core/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.19.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg=="], + "storybook/@storybook/core/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.24.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw=="], - "storybook/@storybook/core/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.19.12", "", { "os": "linux", "cpu": "x64" }, "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg=="], + "storybook/@storybook/core/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.24.2", "", { "os": "linux", "cpu": "x64" }, "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q=="], - "storybook/@storybook/core/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.19.12", "", { "os": "none", "cpu": "x64" }, "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA=="], + "storybook/@storybook/core/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.24.2", "", { "os": "none", "cpu": "arm64" }, "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw=="], - "storybook/@storybook/core/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.19.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw=="], + "storybook/@storybook/core/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.24.2", "", { "os": "none", "cpu": "x64" }, "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw=="], - "storybook/@storybook/core/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.19.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA=="], + "storybook/@storybook/core/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.24.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A=="], - "storybook/@storybook/core/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.19.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A=="], + "storybook/@storybook/core/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.24.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA=="], - "storybook/@storybook/core/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.19.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ=="], + "storybook/@storybook/core/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.24.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig=="], - "storybook/@storybook/core/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.19.12", "", { "os": "win32", "cpu": "x64" }, "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA=="], + "storybook/@storybook/core/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.24.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ=="], + + "storybook/@storybook/core/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.24.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA=="], + + "storybook/@storybook/core/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.24.2", "", { "os": "win32", "cpu": "x64" }, "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg=="], "sucrase/glob/foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], @@ -15059,8 +15021,6 @@ "@babel/register/find-cache-dir/pkg-dir/find-up/locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="], - "@binance/w3w-qrcode-modal/qrcode/yargs/yargs-parser/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], - "@commitlint/top-level/find-up/locate-path/p-locate/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], "@datadog/datadog-ci/ora/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], diff --git a/config/jest-presets/jest/jest-preset.js b/config/jest-presets/jest/jest-preset.js index c43d4e5925c..c151e795496 100644 --- a/config/jest-presets/jest/jest-preset.js +++ b/config/jest-presets/jest/jest-preset.js @@ -34,7 +34,7 @@ module.exports = { // changedSince: 'master', // https://github.com/facebook/jest/issues/2663#issuecomment-341384494 transformIgnorePatterns: [ - 'node_modules/(?!(react-native|react-native-web|react-native-modal-selector|react-native-modal-datetime-picker|react-native-keyboard-controller|@react-navigation|@storybook/react-native|@react-native-community/datetimepicker|react-native-image-colors|uuid|react-native-reanimated|react-native-safe-area-context|react-native-localize|@react-native-masked-view|@statsig-js/js-client|@statsig/react-native-bindings|@statsig/react-bindings|@statsig/js-local-overrides|@react-native|@react-native-firebase|@uniswap/client-embeddedwallet|@uniswap/client-data-api|@uniswap/client-pools|@uniswap/client-platform-service|@connectrpc|@bufbuild|react-native-webview|@gorhom|expo.*|d3-(array|color|format|interpolate|path|scale|shape|time-format|time)|internmap|react-native-qrcode-svg|react-native-modal|react-native-animatable|react-native-masked-view|redux-persist|react-native-url-polyfill|react-native-context-menu-view|react-native-wagmi-charts|react-native-markdown-display|react-native-redash|@walletconnect|moti|react-native-image-picker|wagmi|viem|rn-qr-generator|@solana|jayson)/)', + 'node_modules/(?!(react-native|@universe|react-native-web|react-native-modal-selector|react-native-modal-datetime-picker|react-native-keyboard-controller|@react-navigation|@storybook/react-native|@react-native-community/datetimepicker|react-native-image-colors|uuid|react-native-reanimated|react-native-safe-area-context|react-native-localize|@react-native-masked-view|@statsig-js/js-client|@statsig/react-native-bindings|@statsig/react-bindings|@statsig/js-local-overrides|@react-native|@react-native-firebase|@uniswap/client-embeddedwallet|@uniswap/client-data-api|@uniswap/client-platform-service|@connectrpc|@bufbuild|react-native-webview|@gorhom|expo.*|d3-(array|color|format|interpolate|path|scale|shape|time-format|time)|internmap|react-native-qrcode-svg|react-native-modal|react-native-animatable|react-native-masked-view|redux-persist|react-native-url-polyfill|react-native-context-menu-view|react-native-wagmi-charts|react-native-markdown-display|react-native-redash|@walletconnect|moti|react-native-image-picker|wagmi|viem|rn-qr-generator|@solana|jayson|@uniswap\/client-search)/)', ], collectCoverage: false, // only collect in CI clearMocks: true, diff --git a/config/jest-presets/jest/setup.js b/config/jest-presets/jest/setup.js index acfd62262e2..53cf1b368e0 100644 --- a/config/jest-presets/jest/setup.js +++ b/config/jest-presets/jest/setup.js @@ -109,39 +109,33 @@ const NetInfoStateType = { jest.mock('@react-native-community/netinfo', () => ({ ...mockRNCNetInfo, NetInfoStateType })) -jest.mock('uniswap/src/features/gating/sdk/statsig', () => { - const real = jest.requireActual('uniswap/src/features/gating/sdk/statsig') - const StatsigMock = { - ...real, - useGate: () => { - return { - isLoading: false, - value: false, - } - }, - useConfig: () => { - return {} - }, - +jest.mock('@universe/gating', () => { + const actual = jest.requireActual('@universe/gating') + return { + ...actual, + // Mock functions + useDynamicConfigValue: jest.fn((args) => args.defaultValue), + useFeatureFlag: jest.fn(() => false), + useGate: jest.fn(() => ({ isLoading: false, value: false })), + useConfig: jest.fn(() => ({})), + getStatsigClient: jest.fn(() => ({ + checkGate: jest.fn(() => false), + getConfig: jest.fn(() => ({ + get: (_name, fallback) => fallback, + getValue: (_name, fallback) => fallback, + })), + getLayer: jest.fn(() => ({ + get: jest.fn(() => false), + })), + })), Statsig: { - checkGate: () => false, - getConfig: () => { - return { - get: (_name, fallback) => fallback, - getValue: (_name, fallback) => fallback, - } - }, + checkGate: jest.fn(() => false), + getConfig: jest.fn(() => ({ + get: (_name, fallback) => fallback, + getValue: (_name, fallback) => fallback, + })), }, } - return StatsigMock -}) - -jest.mock('uniswap/src/features/gating/hooks', () => { - const real = jest.requireActual('uniswap/src/features/gating/hooks') - return { - ...real, - useDynamicConfigValue: (args) => args.defaultValue, - } }) // TODO: Remove this mock after mocks in jest-expo are fixed diff --git a/config/vitest-presets/vitest/vitest-preset.js b/config/vitest-presets/vitest/vitest-preset.js index 5b44b8f092d..e08010644ed 100644 --- a/config/vitest-presets/vitest/vitest-preset.js +++ b/config/vitest-presets/vitest/vitest-preset.js @@ -61,7 +61,6 @@ module.exports = { '@react-native-firebase/**', '@uniswap/client-embeddedwallet', '@uniswap/client-data-api', - '@uniswap/client-pools', 'react-native-webview', '@gorhom/**', 'expo*', diff --git a/dangerfile.ts b/dangerfile.ts index a0cdafb2a27..7785ee045d5 100644 --- a/dangerfile.ts +++ b/dangerfile.ts @@ -74,7 +74,7 @@ function checkGeneralizedHookFiles() { } // Put any files here that we explicitly want to ignore! -const IGNORED_SPLIT_RULE_FILES: string[] = ['packages/uniswap/src/features/gating/sdk/statsig.native.ts'] +const IGNORED_SPLIT_RULE_FILES: string[] = ['packages/gating/src/sdk/statsig.native.ts'] function checkSplitFiles() { const touchedFiles = danger.git.modified_files.concat(danger.git.created_files) diff --git a/nx.json b/nx.json index 5f69d8974a1..e24f6a33130 100644 --- a/nx.json +++ b/nx.json @@ -10,7 +10,13 @@ "build": { "dependsOn": ["^prepare", "prepare", "^build"], "inputs": ["dependencies", "sourceFiles", "tsConfig"], - "outputs": ["{projectRoot}/dist", "{projectRoot}/build", "{projectRoot}/.next", "{projectRoot}/types"], + "outputs": [ + "{workspaceRoot}/dist/out-tsc/{projectRoot}", + "{projectRoot}/dist", + "{projectRoot}/build", + "{projectRoot}/.next", + "{projectRoot}/types" + ], "cache": true }, "build:production": { @@ -49,18 +55,14 @@ "options": { "cwd": "{projectRoot}" }, - "dependsOn": [ - "@uniswap/biome-config:prepare" - ] + "dependsOn": ["@uniswap/biome-config:prepare"] }, "lint:biome:fix": { "command": "biome check . --write", "options": { "cwd": "{projectRoot}" }, - "dependsOn": [ - "@uniswap/biome-config:prepare" - ] + "dependsOn": ["@uniswap/biome-config:prepare"] }, "lint:eslint": { "command": "eslint . --ext ts,tsx --max-warnings=0", @@ -82,10 +84,7 @@ }, "lint:fix": { "executor": "nx:noop", - "dependsOn": [ - "lint:biome:fix", - "lint:eslint:fix" - ], + "dependsOn": ["lint:biome:fix", "lint:eslint:fix"], "cache": false }, "test": { @@ -171,13 +170,8 @@ }, "@nx/js:tsc": { "cache": true, - "dependsOn": [ - "^build" - ], - "inputs": [ - "production", - "^production" - ] + "dependsOn": ["^build"], + "inputs": ["production", "^production"] } }, "namedInputs": { diff --git a/package.json b/package.json index 1af9250245b..f42f4722421 100644 --- a/package.json +++ b/package.json @@ -132,11 +132,22 @@ }, "scripts": { "api": "bun run --cwd packages/api", - "clean": "bash ./scripts/clean.sh", - "config": "bun run --cwd packages/config", - "extension": "bun run --cwd apps/extension", "biome-config": "bun run --cwd packages/biome-config", + "config": "bun run --cwd packages/config", "eslint-config": "bun run --cwd packages/eslint-config", + "extension": "bun run --cwd apps/extension", + "mobile": "bun run --cwd apps/mobile", + "sessions": "bun run --cwd packages/sessions", + "ui": "bun run --cwd packages/ui", + "uniswap": "bun run --cwd packages/uniswap", + "utilities": "bun run --cwd packages/utilities", + "wallet": "bun run --cwd packages/wallet", + "web": "bun run --cwd apps/web", + "clean": "bash ./scripts/clean.sh", + "lfg": "bun run g:prepare && bun run mobile env:local:download && bun run extension env:local:download && bun run g:build && bun run mobile pod && bun run mobile ios", + "local:check": "./scripts/local-version-check.sh", + "preinstall": "./scripts/check-bun-version.sh", + "postinstall": "lefthook install && bun g:prepare", "g:build": "nx run-many -t build --parallel", "g:build:storybook": "nx run-many -t storybook:build --skip-nx-cache --parallel", "g:check:deps:usage": "./scripts/check-deps-with-vue-fix.sh && nx run-many -t check:deps:usage", @@ -148,12 +159,13 @@ "g:lint:changed": "nx affected -t lint --base=${NX_BASE:-main} --head=${NX_HEAD:-HEAD}", "g:lint": "nx run-many -t lint", "g:lint:fix": "nx run-many -t lint:fix", + "g:pre-commit-checks": "nx affected -t typecheck,lint:biome --uncommitted --output-style=stream", "g:prepare": "nx run-many -t prepare --output-style=stream", - "g:rm:local-packages": "rm -rf ./node_modules/utilities ./node_modules/wallet ./node_modules/ui ./node_modules/uniswap", + "g:rm:local-packages": "bash ./scripts/remove-local-packages.sh", "g:rm:nodemodules": "rm -rf node_modules **/node_modules", - "g:pre-commit-checks": "nx affected -t typecheck,lint:biome --uncommitted --output-style=stream", - "g:run-fast-checks": "nx affected -t typecheck,lint,build --base=${NX_BASE:-HEAD~1} --head=${NX_HEAD:-HEAD}", "g:run-all-checks": "nx run-many -t typecheck,lint,test,build,check:circular", + "g:run-fast-checks": "nx affected -t typecheck,lint,build --base=${NX_BASE:-HEAD~1} --head=${NX_HEAD:-HEAD}", + "g:snapshots": "nx run-many -t snapshots", "g:test:storybook:standalone": "nx run-many -t storybook:test:standalone --skip-nx-cache --parallel", "g:test": "nx run-many -t test", "g:test:coverage": "nx run-many -t test -- --collectCoverage=true", @@ -167,7 +179,6 @@ "g:test:coverage:extension": "nx test @uniswap/extension -- --collectCoverage=true", "g:test:coverage:mobile": "nx test @uniswap/mobile -- --collectCoverage=true", "g:test:coverage:wallet": "nx test wallet -- --collectCoverage=true", - "g:snapshots": "nx run-many -t snapshots", "g:typecheck": "nx run-many -t typecheck", "g:typecheck:changed": "nx affected -t typecheck --base=${NX_BASE:-main} --head=${NX_HEAD:-HEAD}", "i18n:extract": "i18next", @@ -175,22 +186,12 @@ "i18n:upload": "dotenv -e .env.defaults -c -- bun run i18n:_upload", "i18n:_download": "crowdin download", "i18n:download": "dotenv -e .env.defaults -c -- bun run i18n:_download", - "lfg": "bun run g:prepare && bun run mobile env:local:download && bun run extension env:local:download && bun run g:build && bun run mobile pod && bun run mobile ios", - "mobile": "bun run --cwd apps/mobile", - "local:check": "./scripts/local-version-check.sh", - "preinstall": "./scripts/check-bun-version.sh", - "postinstall": "lefthook install && bun g:prepare", - "sessions": "bun run --cwd packages/sessions", - "ui": "bun run --cwd packages/ui", "upgrade:tamagui": "bun update '*tamagui*' '@tamagui/*'", "upgrade:tamagui:canary": "bun update '*tamagui*'@canary '@tamagui/*'@canary", - "wallet": "bun run --cwd packages/wallet", - "web": "bun run --cwd apps/web", - "uniswap": "bun run --cwd packages/uniswap", - "utilities": "bun run --cwd packages/utilities", - "knip": "knip", "wallet:release:setup-cherry-pick-branches": "bunx tsx ./scripts/wallet-releases/generate-cherry-pick-branches-for-release.ts", - "wallet:release:generate-cherry-pick-commit-command": "bunx tsx ./scripts/wallet-releases/generate-cherry-pick-commit-command.ts" + "wallet:release:generate-cherry-pick-commit-command": "bunx tsx ./scripts/wallet-releases/generate-cherry-pick-commit-command.ts", + "gating": "bun run --cwd packages/gating", + "notifications": "bun run --cwd packages/notifications" }, "workspaces": ["apps/*", "packages/*", "config/*", "tools/uniswap-nx"], "patchedDependencies": { diff --git a/packages/api/package.json b/packages/api/package.json index de6f0dc50d8..2569dd0d52d 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -25,10 +25,9 @@ "@connectrpc/connect": "1.4.0", "@connectrpc/connect-web": "1.4.0", "@tanstack/react-query": "5.77.2", - "@uniswap/client-data-api": "0.0.14", + "@uniswap/client-data-api": "0.0.18", "@uniswap/client-embeddedwallet": "0.0.16", "@uniswap/client-explore": "0.0.17", - "@uniswap/client-pools": "0.0.17", "@uniswap/client-trading": "0.1.0", "@universe/config": "workspace:^", "@universe/sessions": "workspace:^", diff --git a/packages/api/src/clients/graphql/queries.graphql b/packages/api/src/clients/graphql/queries.graphql index 53aca66dce4..a6a92de8984 100644 --- a/packages/api/src/clients/graphql/queries.graphql +++ b/packages/api/src/clients/graphql/queries.graphql @@ -836,6 +836,12 @@ fragment TokenBasicInfoParts on Token { name standard symbol + isBridged + bridgedWithdrawalInfo { + chain + provider + url + } } fragment TokenBasicProjectParts on Token { diff --git a/packages/api/src/clients/graphql/schema.graphql b/packages/api/src/clients/graphql/schema.graphql index 287be283601..37e138268fb 100644 --- a/packages/api/src/clients/graphql/schema.graphql +++ b/packages/api/src/clients/graphql/schema.graphql @@ -1,19 +1,6 @@ """This directive allows results to be deferred during execution""" directive @defer on FIELD -"""Tells the service which mutation triggers this subscription.""" -directive @aws_subscribe( - """ - List of mutations which will trigger this subscription when they are called. - """ - mutations: [String] -) on FIELD_DEFINITION - -""" -Tells the service this field/object has access authorized by an OIDC token. -""" -directive @aws_oidc on OBJECT | FIELD_DEFINITION - """Directs the schema to enforce authorization on a field""" directive @aws_auth( """List of cognito user pool groups which have access on this field""" @@ -21,15 +8,26 @@ directive @aws_auth( ) on FIELD_DEFINITION """ -Tells the service this field/object has access authorized by sigv4 signing. +Tells the service which subscriptions will be published to when this mutation is +called. This directive is deprecated use @aws_susbscribe directive instead. """ -directive @aws_iam on OBJECT | FIELD_DEFINITION +directive @aws_publish( + """ + List of subscriptions which will be published to when this mutation is called. + """ + subscriptions: [String] +) on FIELD_DEFINITION """ Tells the service this field/object has access authorized by an API key. """ directive @aws_api_key on OBJECT | FIELD_DEFINITION +""" +Tells the service this field/object has access authorized by sigv4 signing. +""" +directive @aws_iam on OBJECT | FIELD_DEFINITION + """ Tells the service this field/object has access authorized by a Cognito User Pools token. """ @@ -38,15 +36,12 @@ directive @aws_cognito_user_pools( cognito_groups: [String] ) on OBJECT | FIELD_DEFINITION -""" -Tells the service which subscriptions will be published to when this mutation is -called. This directive is deprecated use @aws_susbscribe directive instead. -""" -directive @aws_publish( +"""Tells the service which mutation triggers this subscription.""" +directive @aws_subscribe( """ - List of subscriptions which will be published to when this mutation is called. + List of mutations which will trigger this subscription when they are called. """ - subscriptions: [String] + mutations: [String] ) on FIELD_DEFINITION """ @@ -54,6 +49,11 @@ Tells the service this field/object has access authorized by a Lambda Authorizer """ directive @aws_lambda on OBJECT | FIELD_DEFINITION +""" +Tells the service this field/object has access authorized by an OIDC token. +""" +directive @aws_oidc on OBJECT | FIELD_DEFINITION + """ Types, unions, and inputs (alphabetized): These are colocated to highlight the relationship between some types and their inputs. @@ -182,6 +182,12 @@ type BlockaidFees { sell: Float } +type BridgedWithdrawalInfo { + chain: String! + provider: String! + url: String! +} + enum Chain { ARBITRUM AVALANCHE @@ -1027,6 +1033,7 @@ enum PriceSource { SUBGRAPH_V2 SUBGRAPH_V3 SUBGRAPH_V4 + EXTERNAL } enum ProtectionAttackType { @@ -1078,8 +1085,8 @@ type Query { dailyProtocolTvl(chain: Chain!, version: ProtocolVersion!): [TimestampedAmount!] """ returns top v3 pools sorted by total value locked in desc order""" - topV3Pools(chain: Chain!, first: Int!, tvlCursor: Float, tokenFilter: String): [V3Pool!] - topV4Pools(chain: Chain!, first: Int!, tvlCursor: Float, tokenFilter: String): [V4Pool!] + topV3Pools(chain: Chain!, first: Int!, tvlCursor: Float, tokenFilter: String, debugMode: Boolean): [V3Pool!] + topV4Pools(chain: Chain!, first: Int!, tvlCursor: Float, tokenFilter: String, debugMode: Boolean): [V4Pool!] v3Pool(chain: Chain!, address: String!): V3Pool v4Pool(chain: Chain!, poolId: String!): V4Pool v3PoolsForTokenPair(chain: Chain!, token0: String!, token1: String!): [V3Pool!] @@ -1088,7 +1095,7 @@ type Query { v4Transactions(chain: Chain!, first: Int!, timestampCursor: Int): [PoolTransaction!] """ returns top v2 pairs sorted by total value locked in desc order""" - topV2Pairs(chain: Chain!, first: Int!, tvlCursor: Float, tokenFilter: String): [V2Pair!] + topV2Pairs(chain: Chain!, first: Int!, tvlCursor: Float, tokenFilter: String, debugMode: Boolean): [V2Pair!] v2Pair(chain: Chain!, address: String!): V2Pair v2Transactions(chain: Chain!, first: Int!, timestampCursor: Int): [PoolTransaction] convert(fromAmount: CurrencyAmountInput!, toCurrency: Currency!): Amount @@ -1226,6 +1233,8 @@ type Token implements IContract { v3Transactions(first: Int!, timestampCursor: Int): [PoolTransaction] v2Transactions(first: Int!, timestampCursor: Int): [PoolTransaction] source: TokenSource + isBridged: Boolean + bridgedWithdrawalInfo: BridgedWithdrawalInfo } type TokenAmount { diff --git a/packages/api/src/clients/notifications/createNotificationsApiClient.ts b/packages/api/src/clients/notifications/createNotificationsApiClient.ts new file mode 100644 index 00000000000..59546503bac --- /dev/null +++ b/packages/api/src/clients/notifications/createNotificationsApiClient.ts @@ -0,0 +1,49 @@ +import type { + GetNotificationsRequest, + InAppNotification, + NotificationsApiClient, + NotificationsClientContext, +} from '@universe/api/src/clients/notifications/types' + +/** + * Factory function to create a NotificationsApiClient + * + * Example usage: + * ```typescript + * const notificationsClient = createNotificationsApiClient({ + * fetchClient: myFetchClient, + * getApiPathPrefix: () => '/notifications/v1' + * }) + * + * const notifications = await notificationsClient.getNotifications() + * ``` + * + * @param ctx - Context containing injected dependencies + * @returns NotificationsApiClient instance + */ +export function createNotificationsApiClient(ctx: NotificationsClientContext): NotificationsApiClient { + const { fetchClient, getApiPathPrefix = (): string => '' } = ctx + + const getNotifications = async (params?: GetNotificationsRequest): Promise => { + const pathPrefix = getApiPathPrefix() + const path = `${pathPrefix}/uniswap.notificationservice.v1.NotificationService/GetNotifications` + + try { + const response = await fetchClient.post<{ notifications: InAppNotification[] }>(path, { + body: JSON.stringify(params ?? {}), + }) + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + return response?.notifications ?? [] + } catch (error) { + // Re-throw with context about which API call failed + throw new Error(`Failed to fetch notifications: ${error instanceof Error ? error.message : String(error)}`, { + cause: error, + }) + } + } + + return { + getNotifications, + } +} diff --git a/packages/api/src/clients/notifications/types.ts b/packages/api/src/clients/notifications/types.ts new file mode 100644 index 00000000000..d0b31810112 --- /dev/null +++ b/packages/api/src/clients/notifications/types.ts @@ -0,0 +1,38 @@ +import { FetchClient } from '@universe/api/src/clients/base/types' + +export interface NotificationsClientContext { + fetchClient: FetchClient + getApiPathPrefix?: () => string +} + +/** + * In-app notification returned by the notifications API + * TODO: This will be replaced with OpenAPI-generated types once the spec is integrated + */ +export interface InAppNotification { + notification_id: string + notification_name: string + meta_data: Record + content: Record + criteria: Record +} + +/** + * Request parameters for fetching notifications + */ +export type GetNotificationsRequest = Record + +/** + * Response from the GetNotifications API endpoint + */ +export interface GetNotificationsResponse { + notifications: InAppNotification[] +} + +export interface NotificationsApiClient { + /** + * Fetch notifications for the current user + * Uses session-based authentication (x-session-id header) via FetchClient + */ + getNotifications: (params?: GetNotificationsRequest) => Promise +} diff --git a/packages/api/src/clients/trading/tradeTypes.ts b/packages/api/src/clients/trading/tradeTypes.ts index 9b0a0cc2a17..7d415342ea1 100644 --- a/packages/api/src/clients/trading/tradeTypes.ts +++ b/packages/api/src/clients/trading/tradeTypes.ts @@ -42,7 +42,7 @@ interface StepProof { orderId?: string } -export interface TradeStep { +export interface PlanStep { stepId: string method: Method payloadType: PayloadType @@ -62,7 +62,7 @@ export interface TradeStep { export interface TradeResponse { tradeId: string - steps: TradeStep[] + steps: PlanStep[] expectedOutput: number timeEstimateMs: number //ms gasFee: string diff --git a/packages/api/src/connectRpc/utils.ts b/packages/api/src/connectRpc/utils.ts index 293daed4462..b26a9acd449 100644 --- a/packages/api/src/connectRpc/utils.ts +++ b/packages/api/src/connectRpc/utils.ts @@ -1,7 +1,7 @@ import { type PlainMessage } from '@bufbuild/protobuf' import { Platform, type PlatformAddress, type WalletAccount } from '@uniswap/client-data-api/dist/data/v1/api_pb' +import { ProtocolVersion } from '@uniswap/client-data-api/dist/data/v1/poolTypes_pb' import { type ProtectionInfo as ProtectionInfoProtobuf } from '@uniswap/client-explore/dist/uniswap/explore/v1/service_pb' -import { ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' import { ProtectionAttackType, type ProtectionInfo, diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 10b3fdd723e..f9a95b99786 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -73,11 +73,11 @@ export { type ExistingTradeRequest, Method, type NewTradeRequest, + type PlanStep, PlanStepStatus, type PriorityQuoteResponse, type SwappableTokensParams, type TradeResponse, - type TradeStep, type UnwrapQuoteResponse, type UpdateExistingTradeRequest, type WrapQuoteResponse, @@ -124,6 +124,16 @@ export { TokenReportEventType, } from '@universe/api/src/clients/data/createDataServiceApiClient' +// Notifications API +export { createNotificationsApiClient } from '@universe/api/src/clients/notifications/createNotificationsApiClient' +export type { + GetNotificationsRequest, + GetNotificationsResponse, + InAppNotification, + NotificationsApiClient, + NotificationsClientContext, +} from '@universe/api/src/clients/notifications/types' + // ConnectRPC API export { ALL_NETWORKS_ARG, diff --git a/packages/biome-config/base.jsonc b/packages/biome-config/base.jsonc index 71b95904adb..2e99ba3aada 100644 --- a/packages/biome-config/base.jsonc +++ b/packages/biome-config/base.jsonc @@ -177,7 +177,17 @@ }, "noUnusedLabels": "error", "noUnusedVariables": "error", - "useExhaustiveDependencies": "error", + "useExhaustiveDependencies": { + "level": "error", + "options": { + "hooks": [ + // React Native Reanimated hooks with stable results + { "name": "useSharedValue", "stableResult": true }, + { "name": "useDerivedValue", "stableResult": true }, + { "name": "useAnimatedRef", "stableResult": true } + ] + } + }, "useHookAtTopLevel": "error", "useIsNan": "warn", "useJsxKeyInIterable": "error", diff --git a/packages/gating/.eslintrc.js b/packages/gating/.eslintrc.js new file mode 100644 index 00000000000..05c00dcf591 --- /dev/null +++ b/packages/gating/.eslintrc.js @@ -0,0 +1,46 @@ +module.exports = { + root: true, + extends: ['@uniswap/eslint-config/native', '@uniswap/eslint-config/webPlatform'], + ignorePatterns: [ + 'node_modules', + '.turbo', + '.eslintrc.js', + 'vitest.config.ts', + 'codegen.ts', + '.nx', + 'scripts', + 'dist', + 'src/**/__generated__', + ], + parserOptions: { + project: 'tsconfig.lint.json', + tsconfigRootDir: __dirname, + ecmaFeatures: { + jsx: true, + }, + ecmaVersion: 2018, + sourceType: 'module', + }, + overrides: [ + { + files: ['src/index.ts'], + rules: { + 'check-file/no-index': 'off', + }, + }, + { + files: ['*.ts', '*.tsx'], + rules: { + 'no-relative-import-paths/no-relative-import-paths': [ + 'error', + { + allowSameFolder: false, + prefix: '@universe/gating', + }, + ], + '@typescript-eslint/prefer-enum-initializers': 'off', + }, + }, + ], + rules: {}, +} diff --git a/packages/gating/README.md b/packages/gating/README.md new file mode 100644 index 00000000000..8b26f751149 --- /dev/null +++ b/packages/gating/README.md @@ -0,0 +1,3 @@ +# @universe/gating + +// TODO diff --git a/packages/gating/package.json b/packages/gating/package.json new file mode 100644 index 00000000000..e8457067531 --- /dev/null +++ b/packages/gating/package.json @@ -0,0 +1,31 @@ +{ + "name": "@universe/gating", + "version": "0.0.0", + "dependencies": { + "@statsig/client-core": "3.12.2", + "@statsig/js-client": "3.12.2", + "@statsig/js-local-overrides": "3.12.2", + "@statsig/react-bindings": "3.12.2", + "@statsig/react-native-bindings": "3.12.2", + "@universe/api": "workspace:*", + "utilities": "workspace:*" + }, + "devDependencies": { + "@types/node": "22.13.1", + "@uniswap/eslint-config": "workspace:^", + "depcheck": "1.4.7", + "eslint": "8.44.0", + "typescript": "5.3.3" + }, + "scripts": { + "typecheck": "nx typecheck gating", + "lint": "nx lint gating", + "lint:fix": "nx lint:fix gating" + }, + "nx": { + "includedScripts": [] + }, + "main": "src/index.ts", + "private": true, + "sideEffects": false +} diff --git a/packages/gating/project.json b/packages/gating/project.json new file mode 100644 index 00000000000..e5b65be81e9 --- /dev/null +++ b/packages/gating/project.json @@ -0,0 +1,16 @@ +{ + "name": "@universe/gating", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/gating/src", + "projectType": "library", + "tags": [], + "targets": { + "typecheck": {}, + "lint:biome": {}, + "lint:biome:fix": {}, + "lint:eslint": {}, + "lint:eslint:fix": {}, + "lint": {}, + "lint:fix": {} + } +} diff --git a/packages/uniswap/src/features/gating/LocalOverrideAdapterWrapper.ts b/packages/gating/src/LocalOverrideAdapterWrapper.ts similarity index 95% rename from packages/uniswap/src/features/gating/LocalOverrideAdapterWrapper.ts rename to packages/gating/src/LocalOverrideAdapterWrapper.ts index 4c4794d7cb0..1c68bee6685 100644 --- a/packages/uniswap/src/features/gating/LocalOverrideAdapterWrapper.ts +++ b/packages/gating/src/LocalOverrideAdapterWrapper.ts @@ -1,5 +1,5 @@ import { LocalOverrideAdapter } from '@statsig/js-local-overrides' -import { getStatsigClient } from 'uniswap/src/features/gating/sdk/statsig' +import { getStatsigClient } from '@universe/gating/src/sdk/statsig' // Workaround for @statsig 3.x.x refreshing client after applying overrides to get the result without reloading // Should be removed after statsig add real time override apply functionality diff --git a/packages/uniswap/src/features/gating/configs.ts b/packages/gating/src/configs.ts similarity index 81% rename from packages/uniswap/src/features/gating/configs.ts rename to packages/gating/src/configs.ts index 6c9ac865d3a..9752b9af739 100644 --- a/packages/uniswap/src/features/gating/configs.ts +++ b/packages/gating/src/configs.ts @@ -1,5 +1,64 @@ import { GasStrategy } from '@universe/api' -import type { Locale } from 'uniswap/src/features/language/constants' + +// TODO: move to own package +export enum Locale { + Afrikaans = 'af-ZA', + ArabicSaudi = 'ar-SA', + Catalan = 'ca-ES', + ChineseSimplified = 'zh-Hans', + ChineseTraditional = 'zh-Hant', + CzechCzechia = 'cs-CZ', + DanishDenmark = 'da-DK', + DutchNetherlands = 'nl-NL', + EnglishUnitedStates = 'en-US', + FinnishFinland = 'fi-FI', + FrenchFrance = 'fr-FR', + GreekGreece = 'el-GR', + HebrewIsrael = 'he-IL', + HindiIndia = 'hi-IN', + HungarianHungarian = 'hu-HU', + IndonesianIndonesia = 'id-ID', + ItalianItaly = 'it-IT', + JapaneseJapan = 'ja-JP', + KoreanKorea = 'ko-KR', + MalayMalaysia = 'ms-MY', + NorwegianNorway = 'no-NO', + PolishPoland = 'pl-PL', + PortugueseBrazil = 'pt-BR', + PortuguesePortugal = 'pt-PT', + RomanianRomania = 'ro-RO', + RussianRussia = 'ru-RU', + Serbian = 'sr-SP', + SpanishLatam = 'es-419', + SpanishBelize = 'es-BZ', + SpanishCuba = 'es-CU', + SpanishDominicanRepublic = 'es-DO', + SpanishGuatemala = 'es-GT', + SpanishHonduras = 'es-HN', + SpanishMexico = 'es-MX', + SpanishNicaragua = 'es-NI', + SpanishPanama = 'es-PA', + SpanishPeru = 'es-PE', + SpanishPuertoRico = 'es-PR', + SpanishElSalvador = 'es-SV', + SpanishUnitedStates = 'es-US', + SpanishArgentina = 'es-AR', + SpanishBolivia = 'es-BO', + SpanishChile = 'es-CL', + SpanishColombia = 'es-CO', + SpanishCostaRica = 'es-CR', + SpanishEcuador = 'es-EC', + SpanishSpain = 'es-ES', + SpanishParaguay = 'es-PY', + SpanishUruguay = 'es-UY', + SpanishVenezuela = 'es-VE', + SwahiliTanzania = 'sw-TZ', + SwedishSweden = 'sv-SE', + TurkishTurkey = 'tr-TR', + UkrainianUkraine = 'uk-UA', + UrduPakistan = 'ur-PK', + VietnameseVietnam = 'vi-VN', +} /** * Dynamic Configs diff --git a/packages/uniswap/src/features/gating/constants.ts b/packages/gating/src/constants.ts similarity index 100% rename from packages/uniswap/src/features/gating/constants.ts rename to packages/gating/src/constants.ts diff --git a/packages/uniswap/src/features/gating/experiments.ts b/packages/gating/src/experiments.ts similarity index 83% rename from packages/uniswap/src/features/gating/experiments.ts rename to packages/gating/src/experiments.ts index abee273dafe..1798d898243 100644 --- a/packages/uniswap/src/features/gating/experiments.ts +++ b/packages/gating/src/experiments.ts @@ -11,10 +11,12 @@ export enum Experiments { UnichainFlashblocksModal = 'unichain_flashblocks_modal', WebFORNudges = 'web_for_nudge', ForFilters = 'for_filters', + PortfolioDisconnectedDemoView = 'portfolio_disconnected_demo_view', } export enum Layers { SwapPage = 'swap-page', + PortfolioPage = 'portfolio-page', } // experiment groups @@ -59,6 +61,10 @@ export enum WebFORNudgesProperties { NudgeEnabled = 'nudgeEnabled', } +export enum PortfolioDisconnectedDemoViewProperties { + DemoViewEnabled = 'demoViewEnabled', +} + export type ExperimentProperties = { [Experiments.PriceUxUpdate]: PriceUxUpdateProperties [Experiments.PrivateRpc]: PrivateRpcProperties @@ -67,6 +73,7 @@ export type ExperimentProperties = { [Experiments.UnichainFlashblocksModal]: UnichainFlashblocksProperties [Experiments.ForFilters]: ForFiltersProperties [Experiments.WebFORNudges]: WebFORNudgesProperties + [Experiments.PortfolioDisconnectedDemoView]: PortfolioDisconnectedDemoViewProperties } // will be a spread of all experiment properties in that layer @@ -75,4 +82,7 @@ export const LayerProperties: Record = { ...PriceUxUpdateProperties, ...UnichainFlashblocksProperties, }), + [Layers.PortfolioPage]: Object.values({ + ...PortfolioDisconnectedDemoViewProperties, + }), } diff --git a/packages/uniswap/src/features/gating/flags.ts b/packages/gating/src/flags.ts similarity index 98% rename from packages/uniswap/src/features/gating/flags.ts rename to packages/gating/src/flags.ts index ae194d53c14..6e0f3b10d5b 100644 --- a/packages/uniswap/src/features/gating/flags.ts +++ b/packages/gating/src/flags.ts @@ -10,6 +10,7 @@ export enum FeatureFlags { // Shared ArbitrumDutchV3, BlockaidFotLogging, + BridgedAssetsBannerV2, ChainedActions, DisableSwap7702, EmbeddedWallet, @@ -87,6 +88,7 @@ export enum FeatureFlags { export const SHARED_FEATURE_FLAG_NAMES = new Map([ [FeatureFlags.ArbitrumDutchV3, 'uniswapx_dutchv3_orders_arbitrum'], [FeatureFlags.BlockaidFotLogging, 'blockaid_fot_logging'], + [FeatureFlags.BridgedAssetsBannerV2, 'bridged_assets_banner_v2'], [FeatureFlags.ChainedActions, 'enable_chained_actions'], [FeatureFlags.DisableSwap7702, 'disable-swap-7702'], [FeatureFlags.EmbeddedWallet, 'embedded_wallet'], diff --git a/packages/uniswap/src/features/gating/getStatsigEnvName.ts b/packages/gating/src/getStatsigEnvName.ts similarity index 100% rename from packages/uniswap/src/features/gating/getStatsigEnvName.ts rename to packages/gating/src/getStatsigEnvName.ts diff --git a/packages/uniswap/src/features/gating/hooks.ts b/packages/gating/src/hooks.ts similarity index 95% rename from packages/uniswap/src/features/gating/hooks.ts rename to packages/gating/src/hooks.ts index d369a0a6e3c..460af0075ec 100644 --- a/packages/uniswap/src/features/gating/hooks.ts +++ b/packages/gating/src/hooks.ts @@ -1,8 +1,7 @@ import { StatsigClientEventCallback, StatsigLoadingStatus } from '@statsig/client-core' -import { useEffect, useMemo, useState } from 'react' -import { DynamicConfigKeys } from 'uniswap/src/features/gating/configs' -import { ExperimentProperties, Experiments } from 'uniswap/src/features/gating/experiments' -import { FeatureFlags, getFeatureFlagName } from 'uniswap/src/features/gating/flags' +import { DynamicConfigKeys } from '@universe/gating/src/configs' +import { ExperimentProperties, Experiments } from '@universe/gating/src/experiments' +import { FeatureFlags, getFeatureFlagName } from '@universe/gating/src/flags' import { getStatsigClient, TypedReturn, @@ -12,7 +11,8 @@ import { useGateValue, useLayer, useStatsigClient, -} from 'uniswap/src/features/gating/sdk/statsig' +} from '@universe/gating/src/sdk/statsig' +import { useEffect, useMemo, useState } from 'react' import { logger } from 'utilities/src/logger/logger' export function useFeatureFlag(flag: FeatureFlags): boolean { diff --git a/packages/gating/src/index.ts b/packages/gating/src/index.ts new file mode 100644 index 00000000000..efe852ff1f6 --- /dev/null +++ b/packages/gating/src/index.ts @@ -0,0 +1,86 @@ +export type { + DatadogIgnoredErrorsValType, + DatadogSessionSampleRateValType, + DeepLinkUrlAllowlist, + DynamicConfigKeys, + ForceUpgradeStatus, + ForceUpgradeTranslations, + GasStrategies, + GasStrategyType, + GasStrategyWithConditions, + UwULinkAllowlist, + UwULinkAllowlistItem, +} from '@universe/gating/src/configs' +export { + AllowedV4WethHookAddressesConfigKey, + BlockedAsyncSubmissionChainIdsConfigKey, + ChainsConfigKey, + DatadogIgnoredErrorsConfigKey, + DatadogSessionSampleRateKey, + DeepLinkUrlAllowlistConfigKey, + DynamicConfigs, + EmbeddedWalletConfigKey, + ExtensionBiometricUnlockConfigKey, + ExternallyConnectableExtensionConfigKey, + ForceUpgradeConfigKey, + HomeScreenExploreTokensConfigKey, + LPConfigKey, + NetworkRequestsConfigKey, + OnDeviceRecoveryConfigKey, + OutageBannerChainIdConfigKey, + SwapConfigKey, + SyncTransactionSubmissionChainIdsConfigKey, + UwuLinkConfigKey, +} from '@universe/gating/src/configs' +export { StatsigCustomAppValue } from '@universe/gating/src/constants' +export type { ExperimentProperties } from '@universe/gating/src/experiments' +export { + Experiments, + ForFiltersProperties, + LayerProperties, + Layers, + NativeTokenPercentageBufferProperties, + PortfolioDisconnectedDemoViewProperties, + PriceUxUpdateProperties, + PrivateRpcProperties, + UnichainFlashblocksProperties, + WebFORNudgesProperties, +} from '@universe/gating/src/experiments' +export { + FeatureFlags, + getFeatureFlagName, + WALLET_FEATURE_FLAG_NAMES, + WEB_FEATURE_FLAG_NAMES, +} from '@universe/gating/src/flags' +export { getStatsigEnvName } from '@universe/gating/src/getStatsigEnvName' +export { + getDynamicConfigValue, + getExperimentValue, + getExperimentValueFromLayer, + getFeatureFlag, + useDynamicConfigValue, + useExperimentValue, + useExperimentValueFromLayer, + useFeatureFlag, + useFeatureFlagWithExposureLoggingDisabled, + useFeatureFlagWithLoading, + useStatsigClientStatus, +} from '@universe/gating/src/hooks' +export { LocalOverrideAdapterWrapper } from '@universe/gating/src/LocalOverrideAdapterWrapper' +export type { + StatsigOptions, + StatsigUser, + StorageProvider, +} from '@universe/gating/src/sdk/statsig' +export { + getOverrideAdapter, + getStatsigClient, + StatsigClient, + StatsigContext, + StatsigProvider, + Storage, + useClientAsyncInit, + useExperiment, + useLayer, +} from '@universe/gating/src/sdk/statsig' +export { getOverrides } from '@universe/gating/src/utils' diff --git a/packages/uniswap/src/features/gating/sdk/statsig.native.ts b/packages/gating/src/sdk/statsig.native.ts similarity index 83% rename from packages/uniswap/src/features/gating/sdk/statsig.native.ts rename to packages/gating/src/sdk/statsig.native.ts index f946807b8dd..8e0c14b1022 100644 --- a/packages/uniswap/src/features/gating/sdk/statsig.native.ts +++ b/packages/gating/src/sdk/statsig.native.ts @@ -1,7 +1,9 @@ import { StatsigClient } from '@statsig/react-bindings' import { StatsigClientRN } from '@statsig/react-native-bindings' -import { config } from 'uniswap/src/config' -import { LocalOverrideAdapterWrapper } from 'uniswap/src/features/gating/LocalOverrideAdapterWrapper' +import { getConfig } from '@universe/config' +import { LocalOverrideAdapterWrapper } from '@universe/gating/src/LocalOverrideAdapterWrapper' + +const config = getConfig() export { StatsigClient, diff --git a/packages/uniswap/src/features/gating/sdk/statsig.ts b/packages/gating/src/sdk/statsig.ts similarity index 92% rename from packages/uniswap/src/features/gating/sdk/statsig.ts rename to packages/gating/src/sdk/statsig.ts index 231452cfe31..df1964153b0 100644 --- a/packages/uniswap/src/features/gating/sdk/statsig.ts +++ b/packages/gating/src/sdk/statsig.ts @@ -1,5 +1,5 @@ import { StatsigClient } from '@statsig/react-bindings' -import { LocalOverrideAdapterWrapper } from 'uniswap/src/features/gating/LocalOverrideAdapterWrapper' +import { LocalOverrideAdapterWrapper } from '@universe/gating/src/LocalOverrideAdapterWrapper' export { StatsigClient, diff --git a/packages/uniswap/src/features/gating/utils.ts b/packages/gating/src/utils.ts similarity index 92% rename from packages/uniswap/src/features/gating/utils.ts rename to packages/gating/src/utils.ts index d06e7398daa..491fc37ff2a 100644 --- a/packages/uniswap/src/features/gating/utils.ts +++ b/packages/gating/src/utils.ts @@ -1,5 +1,5 @@ import { PrecomputedEvaluationsInterface } from '@statsig/js-client' -import { getOverrideAdapter } from 'uniswap/src/features/gating/sdk/statsig' +import { getOverrideAdapter } from '@universe/gating/src/sdk/statsig' export function isStatsigReady(client: PrecomputedEvaluationsInterface): boolean { return client.loadingStatus === 'Ready' diff --git a/packages/gating/tsconfig.json b/packages/gating/tsconfig.json new file mode 100644 index 00000000000..1d8d402436b --- /dev/null +++ b/packages/gating/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../config/tsconfig/app.json", + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.json", "src/global.d.ts"], + "exclude": ["src/**/*.spec.ts", "src/**/*.spec.tsx", "src/**/*.test.ts", "src/**/*.test.tsx"], + "compilerOptions": { + "noEmit": false, + "emitDeclarationOnly": true, + "types": ["node"], + "paths": {} + }, + "references": [ + { + "path": "../utilities" + }, + { + "path": "../api" + } + ] +} diff --git a/packages/gating/tsconfig.lint.json b/packages/gating/tsconfig.lint.json new file mode 100644 index 00000000000..79659c26038 --- /dev/null +++ b/packages/gating/tsconfig.lint.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "preserveSymlinks": true + }, + "include": ["**/*.ts", "**/*.tsx", "**/*.json"], + "exclude": ["node_modules"] +} diff --git a/packages/notifications/.eslintrc.js b/packages/notifications/.eslintrc.js new file mode 100644 index 00000000000..a6dbbad9fee --- /dev/null +++ b/packages/notifications/.eslintrc.js @@ -0,0 +1,45 @@ +module.exports = { + root: true, + extends: ['@uniswap/eslint-config/native', '@uniswap/eslint-config/webPlatform'], + ignorePatterns: [ + 'node_modules', + '.turbo', + '.eslintrc.js', + 'vitest.config.ts', + 'codegen.ts', + '.nx', + 'scripts', + 'dist', + 'src/**/__generated__', + ], + parserOptions: { + project: 'tsconfig.lint.json', + tsconfigRootDir: __dirname, + ecmaFeatures: { + jsx: true, + }, + ecmaVersion: 2018, + sourceType: 'module', + }, + overrides: [ + { + files: ['src/index.ts'], + rules: { + 'check-file/no-index': 'off', + }, + }, + { + files: ['*.ts', '*.tsx'], + rules: { + 'no-relative-import-paths/no-relative-import-paths': [ + 'error', + { + allowSameFolder: false, + prefix: '@universe/notifications', + }, + ], + }, + }, + ], + rules: {}, +} diff --git a/packages/notifications/README.md b/packages/notifications/README.md new file mode 100644 index 00000000000..afbab0c0cfb --- /dev/null +++ b/packages/notifications/README.md @@ -0,0 +1,73 @@ +# @universe/notifications + +Client-side notification system for fetching, processing, storing, and displaying notifications from a backend service. + +## Architecture + +``` +NotificationSystem (orchestrator) +├── NotificationDataSource → Fetch/websocket notification data +├── NotificationTracker → Track shown/dismissed state +├── NotificationProcessor → Filter & prioritize notifications +├── NotificationChainCoordinator → Handle multi-step notification flows +└── NotificationRenderer → Platform-specific UI rendering +``` + +## Core Concepts + +### Notification Chains +Notifications can trigger follow-up notifications based on user actions: +```json +{ + "notificationName": "welcome_step_1", + "content": { + "buttons": [{ + "text": "Next", + "onClickType": "ON_CLICK_TYPE_DISMISS_AND_POPUP", + "onClickLink": "welcome_step_2" // ← triggers next notification + }] + } +} +``` + +## Usage + +### Initialize the System + +```typescript +import { createNotificationSystem } from '@universe/notifications' + +const notificationSystem = createNotificationSystem({ + dataSources: [getFetchNotificationDataSource({ apiClient })], + tracker: createLocalNotificationTracker({ storageDriver }), + processor: createNotificationProcessor(), + renderer: createNotificationRenderer(), + chainCoordinator: createNotificationChainCoordinator() +}) + +await notificationSystem.initialize() +``` + +### Handle User Actions + +```typescript +// When user clicks a button +notificationSystem.onButtonClick(notificationName, button) + +// When user dismisses +notificationSystem.onDismiss(notificationName) + +// When user clicks background +notificationSystem.onBackgroundClick(notificationName) +``` + +### React Integration + +```tsx +// Mount container at app root + + +// Container reads from Zustand store +const activeNotifications = useNotificationStore(state => state.activeNotifications) +const notificationSystem = useNotificationStore(state => state.notificationSystem) +``` diff --git a/packages/notifications/package.json b/packages/notifications/package.json new file mode 100644 index 00000000000..fdcf387b687 --- /dev/null +++ b/packages/notifications/package.json @@ -0,0 +1,18 @@ +{ + "name": "@universe/notifications", + "version": "0.0.0", + "devDependencies": { + "@types/node": "22.13.1", + "@uniswap/eslint-config": "workspace:^", + "depcheck": "1.4.7", + "eslint": "8.44.0", + "typescript": "5.3.3" + }, + "scripts": {}, + "nx": { + "includedScripts": [] + }, + "main": "src/index.ts", + "private": true, + "sideEffects": false +} diff --git a/packages/notifications/project.json b/packages/notifications/project.json new file mode 100644 index 00000000000..83f11f630fe --- /dev/null +++ b/packages/notifications/project.json @@ -0,0 +1,16 @@ +{ + "name": "@universe/notifications", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/notifications/src", + "projectType": "library", + "tags": [], + "targets": { + "typecheck": {}, + "lint:biome": {}, + "lint:biome:fix": {}, + "lint:eslint": {}, + "lint:eslint:fix": {}, + "lint": {}, + "lint:fix": {} + } +} diff --git a/packages/notifications/src/index.ts b/packages/notifications/src/index.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/notifications/tsconfig.json b/packages/notifications/tsconfig.json new file mode 100644 index 00000000000..6fbf4aa3e1b --- /dev/null +++ b/packages/notifications/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../config/tsconfig/app.json", + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.json", "src/global.d.ts"], + "exclude": ["src/**/*.spec.ts", "src/**/*.spec.tsx", "src/**/*.test.ts", "src/**/*.test.tsx"], + "compilerOptions": { + "noEmit": false, + "emitDeclarationOnly": true, + "types": ["node"], + "paths": {} + }, + "references": [] +} diff --git a/packages/notifications/tsconfig.lint.json b/packages/notifications/tsconfig.lint.json new file mode 100644 index 00000000000..79659c26038 --- /dev/null +++ b/packages/notifications/tsconfig.lint.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "preserveSymlinks": true + }, + "include": ["**/*.ts", "**/*.tsx", "**/*.json"], + "exclude": ["node_modules"] +} diff --git a/packages/ui/src/assets/graphics/bridged-assets-v2-web-banner.png b/packages/ui/src/assets/graphics/bridged-assets-v2-web-banner.png new file mode 100644 index 0000000000000000000000000000000000000000..c7e87c3aa9006d2308279bfa82b69c8be89ef245 GIT binary patch literal 128449 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

2^rd?|>O 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 { return { diff --git a/packages/ui/src/components/switch/Switch.native.tsx b/packages/ui/src/components/switch/Switch.native.tsx index e5023ae2892..6103f25683a 100644 --- a/packages/ui/src/components/switch/Switch.native.tsx +++ b/packages/ui/src/components/switch/Switch.native.tsx @@ -40,7 +40,7 @@ export const Switch = memo(function Switch({ if (checked !== undefined && checked !== (progress.value === 1)) { progress.value = withTiming(checked ? 1 : 0, ANIMATION_CONFIG) } - }, [checked, progress]) + }, [checked]) const trackStyle = useAnimatedStyle(() => { const isOn = progress.value diff --git a/packages/ui/src/loading/Shine.native.tsx b/packages/ui/src/loading/Shine.native.tsx index 34c98d0ba77..c43c1db9cb1 100644 --- a/packages/ui/src/loading/Shine.native.tsx +++ b/packages/ui/src/loading/Shine.native.tsx @@ -30,7 +30,7 @@ export function Shine({ shimmerDurationSeconds = 2, children, disabled }: ShineP useEffect(() => { xPosition.value = withRepeat(withTiming(1, { duration: shimmerDuration }), Infinity, false) - }, [xPosition, shimmerDuration]) + }, [shimmerDuration]) const animatedStyle = useAnimatedStyle(() => ({ ...StyleSheet.absoluteFillObject, diff --git a/packages/ui/src/loading/Skeleton.native.tsx b/packages/ui/src/loading/Skeleton.native.tsx index 996ab35d086..37629c16a11 100644 --- a/packages/ui/src/loading/Skeleton.native.tsx +++ b/packages/ui/src/loading/Skeleton.native.tsx @@ -19,7 +19,6 @@ export function Skeleton({ children, contrast, disabled }: SkeletonProps): JSX.E const [layout, setLayout] = useState() const xPosition = useSharedValue(0) - // biome-ignore lint/correctness/useExhaustiveDependencies: only want to do this once on mount useLayoutEffect(() => { // TODO: [MOB-210] tweak animation to be smoother, right now sometimes looks kind of stuttery xPosition.value = withRepeat(withTiming(1, { duration: SHIMMER_DURATION }), Infinity, true) diff --git a/packages/ui/src/loading/SpinningLoader.native.tsx b/packages/ui/src/loading/SpinningLoader.native.tsx index bfaf4dc085d..44a3cfbf01d 100644 --- a/packages/ui/src/loading/SpinningLoader.native.tsx +++ b/packages/ui/src/loading/SpinningLoader.native.tsx @@ -33,7 +33,7 @@ export function SpinningLoader({ size = 20, disabled, color }: SpinningLoaderPro -1, ) return () => cancelAnimation(rotation) - }, [rotation]) + }, []) if (disabled) { return diff --git a/packages/uniswap/jest-package-mocks.js b/packages/uniswap/jest-package-mocks.js index 4141f641fdd..970956be0fd 100644 --- a/packages/uniswap/jest-package-mocks.js +++ b/packages/uniswap/jest-package-mocks.js @@ -20,10 +20,10 @@ jest.mock('utilities/src/device/uniqueId', () => { return jest.requireActual('uniswap/src/test/mocks/uniqueId') }) -jest.mock('uniswap/src/features/gating/sdk/statsig', () => { - const actualStatsig = jest.requireActual('uniswap/src/features/gating/sdk/statsig') +jest.mock('@universe/gating', () => { + const actual = jest.requireActual('@universe/gating') return { - ...actualStatsig, + ...actual, useClientAsyncInit: jest.fn(() => ({ client: null, isLoading: true, diff --git a/packages/uniswap/package.json b/packages/uniswap/package.json index a0e09b69a13..9d4bae0b472 100644 --- a/packages/uniswap/package.json +++ b/packages/uniswap/package.json @@ -26,6 +26,7 @@ "@connectrpc/connect-query": "1.4.1", "@datadog/browser-logs": "5.20.0", "@datadog/browser-rum": "5.23.3", + "@ethersproject/abstract-provider": "5.8.0", "@ethersproject/abstract-signer": "5.7.0", "@ethersproject/address": "5.7.0", "@ethersproject/bignumber": "5.7.0", @@ -40,21 +41,15 @@ "@shopify/flash-list": "1.7.3", "@simplewebauthn/browser": "13.1.0", "@solana/web3.js": "1.92.0", - "@statsig/client-core": "3.12.2", - "@statsig/js-client": "3.12.2", - "@statsig/js-local-overrides": "3.12.2", - "@statsig/react-bindings": "3.12.2", - "@statsig/react-native-bindings": "3.12.2", "@tanstack/query-async-storage-persister": "5.51.21", "@tanstack/react-query": "5.77.2", "@tanstack/react-query-persist-client": "5.77.2", "@typechain/ethers-v5": "7.2.0", "@types/poisson-disk-sampling": "2.2.4", "@uniswap/analytics-events": "2.43.0", - "@uniswap/client-data-api": "0.0.14", + "@uniswap/client-data-api": "0.0.18", "@uniswap/client-embeddedwallet": "0.0.16", "@uniswap/client-explore": "0.0.17", - "@uniswap/client-pools": "0.0.17", "@uniswap/client-search": "0.0.10", "@uniswap/client-trading": "0.1.0", "@uniswap/permit2-sdk": "1.3.0", @@ -67,6 +62,7 @@ "@uniswap/v4-sdk": "1.21.2", "@universe/api": "workspace:^", "@universe/config": "workspace:^", + "@universe/gating": "workspace:^", "apollo-link-rest": "0.9.0", "date-fns": "2.30.0", "dayjs": "1.11.7", diff --git a/packages/uniswap/src/components/BridgedAsset/BridgedAssetModal.tsx b/packages/uniswap/src/components/BridgedAsset/BridgedAssetModal.tsx index ab6b1161337..5aab811db58 100644 --- a/packages/uniswap/src/components/BridgedAsset/BridgedAssetModal.tsx +++ b/packages/uniswap/src/components/BridgedAsset/BridgedAssetModal.tsx @@ -17,7 +17,6 @@ import { EnvelopeHeart } from 'ui/src/components/icons/EnvelopeHeart' import { OrderRouting } from 'ui/src/components/icons/OrderRouting' import { Verified } from 'ui/src/components/icons/Verified' import { iconSizes } from 'ui/src/theme' -import { getBridgedAsset } from 'uniswap/src/components/BridgedAsset/utils' import { CurrencyLogo } from 'uniswap/src/components/CurrencyLogo/CurrencyLogo' import { Modal } from 'uniswap/src/components/modals/Modal' import { uniswapUrls } from 'uniswap/src/constants/urls' @@ -47,8 +46,7 @@ export const BridgedAssetModalAtom = atom(un function BridgedAssetModalContent({ currencyInfo }: { currencyInfo: CurrencyInfo }): JSX.Element | null { const { t } = useTranslation() const chainName = getChainLabel(currencyInfo.currency.chainId) - const bridgedAsset = getBridgedAsset(currencyInfo) - if (!currencyInfo.currency.symbol || !bridgedAsset) { + if (!currencyInfo.currency.symbol || !currencyInfo.isBridged) { return null } @@ -104,11 +102,13 @@ function BridgedAssetModalContent({ currencyInfo }: { currencyInfo: CurrencyInfo - {t('bridgedAsset.modal.feature.withdrawToNativeChain', { nativeChainName: bridgedAsset.nativeChain })} + {t('bridgedAsset.modal.feature.withdrawToNativeChain', { + nativeChainName: currencyInfo.bridgedWithdrawalInfo?.chain ?? '', + })} {t('bridgedAsset.modal.feature.withdrawToNativeChain.description', { - nativeChainName: bridgedAsset.nativeChain, + nativeChainName: currencyInfo.bridgedWithdrawalInfo?.chain ?? '', })} diff --git a/packages/uniswap/src/components/BridgedAsset/WormholeModal.tsx b/packages/uniswap/src/components/BridgedAsset/WormholeModal.tsx index 38973fe1773..394941a0b6f 100644 --- a/packages/uniswap/src/components/BridgedAsset/WormholeModal.tsx +++ b/packages/uniswap/src/components/BridgedAsset/WormholeModal.tsx @@ -17,7 +17,6 @@ import { ExternalLink } from 'ui/src/components/icons/ExternalLink' import { Shuffle } from 'ui/src/components/icons/Shuffle' import { iconSizes } from 'ui/src/theme' import { BaseModalProps } from 'uniswap/src/components/BridgedAsset/BridgedAssetModal' -import { getBridgedAsset } from 'uniswap/src/components/BridgedAsset/utils' import { CurrencyLogo } from 'uniswap/src/components/CurrencyLogo/CurrencyLogo' import { Modal } from 'uniswap/src/components/modals/Modal' import { uniswapUrls } from 'uniswap/src/constants/urls' @@ -52,7 +51,7 @@ export function WormholeModal({ const textColor = useMemo(() => { return getContrastPassingTextColor(validTokenColor ?? colors.accent1.val) }, [colors.accent1.val, validTokenColor]) - const bridgedAsset = getBridgedAsset(currencyInfo) + const bridgedWithdrawalInfo = currencyInfo?.bridgedWithdrawalInfo const onPressLearnMore = async (): Promise => { await openUri({ uri: uniswapUrls.helpArticleUrls.bridgedAssets }) @@ -60,14 +59,17 @@ export function WormholeModal({ } const onPressContinue = useEvent(async () => { + if (!bridgedWithdrawalInfo?.url) { + return + } await openUri({ - uri: `${uniswapUrls.wormholeUrl}?sourceChain=unichain&targetChain=${bridgedAsset?.nativeChain}&asset=${bridgedAsset?.unichainAddress}&targetAsset=${bridgedAsset?.nativeAddress}`, + uri: bridgedWithdrawalInfo.url, openExternalBrowser: true, }) onClose() }) - if (!currencyInfo || !currencyInfo.currency.symbol || !bridgedAsset) { + if (!currencyInfo || !currencyInfo.currency.symbol || !bridgedWithdrawalInfo) { return null } const chainName = getChainLabel(currencyInfo.currency.chainId) @@ -127,14 +129,15 @@ export function WormholeModal({ {t('bridgedAsset.wormhole.title', { currencySymbol: currencyInfo.currency.symbol, - nativeChainName: bridgedAsset.nativeChain, + nativeChainName: bridgedWithdrawalInfo.chain, })} {t('bridgedAsset.wormhole.description', { currencySymbol: currencyInfo.currency.symbol, chainName, - nativeChainName: bridgedAsset.nativeChain, + nativeChainName: bridgedWithdrawalInfo.chain, + provider: bridgedWithdrawalInfo.provider, })} @@ -158,7 +161,7 @@ export function WormholeModal({ onPress={onPressContinue} > - {t('bridgedAsset.wormhole.button')} + {t('bridgedAsset.wormhole.button', { provider: bridgedWithdrawalInfo.provider })} diff --git a/packages/uniswap/src/components/BridgedAsset/utils.ts b/packages/uniswap/src/components/BridgedAsset/utils.ts deleted file mode 100644 index c74ddbe26d9..00000000000 --- a/packages/uniswap/src/components/BridgedAsset/utils.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { BridgedAsset, isBridgedAsset, UNICHAIN_BRIDGED_ASSETS } from 'uniswap/src/constants/tokens' -import { UniverseChainId } from 'uniswap/src/features/chains/types' -import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' - -export function checkIsBridgedAsset(currencyInfo?: CurrencyInfo): boolean { - if (!currencyInfo) { - return false - } - - return ( - currencyInfo.currency.chainId === UniverseChainId.Unichain && - currencyInfo.currency.isToken && - isBridgedAsset(currencyInfo.currency.address) - ) -} - -export function getBridgedAsset(currencyInfo?: Maybe): BridgedAsset | undefined { - if (!currencyInfo || !currencyInfo.currency.isToken) { - return undefined - } - const address = currencyInfo.currency.address - return UNICHAIN_BRIDGED_ASSETS.find((asset) => asset.unichainAddress === address) -} diff --git a/packages/uniswap/src/components/CurrencyInputPanel/AmountInputPresets/AmountInputPresets.tsx b/packages/uniswap/src/components/CurrencyInputPanel/AmountInputPresets/AmountInputPresets.tsx index 1b3889b53ea..1103debb35d 100644 --- a/packages/uniswap/src/components/CurrencyInputPanel/AmountInputPresets/AmountInputPresets.tsx +++ b/packages/uniswap/src/components/CurrencyInputPanel/AmountInputPresets/AmountInputPresets.tsx @@ -1,21 +1,17 @@ -import { Flex, FlexProps } from 'ui/src' +import { Key } from 'react' +import { ButtonProps, Flex, FlexProps } from 'ui/src' import { get200MsAnimationDelayFromIndex } from 'ui/src/theme/animations/delay200ms' -import { PresetAmountButton } from 'uniswap/src/components/CurrencyInputPanel/AmountInputPresets/PresetAmountButton' import { AmountInputPresetsProps } from 'uniswap/src/components/CurrencyInputPanel/AmountInputPresets/types' -import { PRESET_PERCENTAGES } from 'uniswap/src/components/CurrencyInputPanel/AmountInputPresets/utils' -import { ElementName } from 'uniswap/src/features/telemetry/constants' -import { CurrencyField } from 'uniswap/src/types/currency' import { isHoverable } from 'utilities/src/platform' -export function AmountInputPresets({ +export const PRESET_BUTTON_PROPS: ButtonProps = { variant: 'default', py: '$spacing4' } + +export function AmountInputPresets({ hoverLtr, - currencyAmount, - currencyBalance, - transactionType, - buttonProps, - onSetPresetValue, + presets, + renderPreset, ...rest -}: AmountInputPresetsProps & FlexProps): JSX.Element { +}: AmountInputPresetsProps & FlexProps): JSX.Element { return ( - {PRESET_PERCENTAGES.map((percent, index) => ( + {presets.map((preset, index) => ( - + {renderPreset(preset)} ))} diff --git a/packages/uniswap/src/components/CurrencyInputPanel/AmountInputPresets/types.ts b/packages/uniswap/src/components/CurrencyInputPanel/AmountInputPresets/types.ts index 8fda416c162..7734ff991db 100644 --- a/packages/uniswap/src/components/CurrencyInputPanel/AmountInputPresets/types.ts +++ b/packages/uniswap/src/components/CurrencyInputPanel/AmountInputPresets/types.ts @@ -1,15 +1,8 @@ -import { Currency, CurrencyAmount } from '@uniswap/sdk-core' -import { ButtonProps } from 'ui/src/components/buttons/Button/types' -import { TransactionType } from 'uniswap/src/features/transactions/types/transactionDetails' - export type PresetPercentageNumber = 25 | 50 | 75 | 100 export type PresetPercentage = PresetPercentageNumber | 'max' -export interface AmountInputPresetsProps { +export interface AmountInputPresetsProps { hoverLtr?: boolean - currencyAmount: CurrencyAmount | null | undefined - currencyBalance: CurrencyAmount - transactionType?: TransactionType - buttonProps?: ButtonProps - onSetPresetValue: (amount: string, percentage: PresetPercentage) => void + presets: T[] + renderPreset: (preset: T) => JSX.Element } diff --git a/packages/uniswap/src/components/CurrencyInputPanel/CurrencyInputPanel.tsx b/packages/uniswap/src/components/CurrencyInputPanel/CurrencyInputPanel.tsx index 194d817d833..29f5a8e6bb9 100644 --- a/packages/uniswap/src/components/CurrencyInputPanel/CurrencyInputPanel.tsx +++ b/packages/uniswap/src/components/CurrencyInputPanel/CurrencyInputPanel.tsx @@ -1,15 +1,20 @@ /* eslint-disable complexity */ import { forwardRef, memo, useCallback } from 'react' import { Flex, TouchableArea, useIsShortMobileDevice, useShakeAnimation } from 'ui/src' -import { AmountInputPresets } from 'uniswap/src/components/CurrencyInputPanel/AmountInputPresets/AmountInputPresets' +import { + AmountInputPresets, + PRESET_BUTTON_PROPS, +} from 'uniswap/src/components/CurrencyInputPanel/AmountInputPresets/AmountInputPresets' import { PresetAmountButton } from 'uniswap/src/components/CurrencyInputPanel/AmountInputPresets/PresetAmountButton' import type { PresetPercentage } from 'uniswap/src/components/CurrencyInputPanel/AmountInputPresets/types' +import { PRESET_PERCENTAGES } from 'uniswap/src/components/CurrencyInputPanel/AmountInputPresets/utils' import { CurrencyInputPanelBalance } from 'uniswap/src/components/CurrencyInputPanel/CurrencyInputPanelBalance' import { CurrencyInputPanelHeader } from 'uniswap/src/components/CurrencyInputPanel/CurrencyInputPanelHeader' import { CurrencyInputPanelInput } from 'uniswap/src/components/CurrencyInputPanel/CurrencyInputPanelInput' import { CurrencyInputPanelValue } from 'uniswap/src/components/CurrencyInputPanel/CurrencyInputPanelValue' import { useIndicativeQuoteTextDisplay } from 'uniswap/src/components/CurrencyInputPanel/hooks/useIndicativeQuoteTextDisplay' import type { CurrencyInputPanelProps, CurrencyInputPanelRef } from 'uniswap/src/components/CurrencyInputPanel/types' +import { ElementName } from 'uniswap/src/features/telemetry/constants' import { useWallet } from 'uniswap/src/features/wallet/hooks/useWallet' import { CurrencyField } from 'uniswap/src/types/currency' import { isExtensionApp, isMobileWeb, isWebAppDesktop } from 'utilities/src/platform' @@ -80,6 +85,22 @@ export const CurrencyInputPanel = memo( [onSetPresetValue], ) + const renderPreset = useCallback( + (preset: PresetPercentage) => ( + + ), + [currencyAmount, currencyBalance, currencyField, handleSetPresetValue, transactionType], + ) + return ( {showPercentagePresetsOnBottom && currencyBalance && !currencyAmount ? ( - + ) : ( ( + + ), + [currencyAmount, currencyBalance, currencyField, onSetPresetValue], + ) + if (!headerLabel && !showDefaultTokenOptions) { return null } @@ -49,12 +71,7 @@ export function CurrencyInputPanelHeader({ {showInputPresets && ( - + )} {showDefaultTokenOptions && isWebAppDesktop && ( diff --git a/packages/uniswap/src/components/TokenSelector/TokenSelector.tsx b/packages/uniswap/src/components/TokenSelector/TokenSelector.tsx index 8a11c2310d4..ca096c964fd 100644 --- a/packages/uniswap/src/components/TokenSelector/TokenSelector.tsx +++ b/packages/uniswap/src/components/TokenSelector/TokenSelector.tsx @@ -1,5 +1,6 @@ import type { BottomSheetView } from '@gorhom/bottom-sheet' import { Currency } from '@uniswap/sdk-core' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { hasStringAsync } from 'expo-clipboard' import { ComponentProps, memo, useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -23,8 +24,6 @@ import { TradeableAsset } from 'uniswap/src/entities/assets' import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { SearchContext } from 'uniswap/src/features/search/SearchModal/analytics/SearchContext' import { useFilterCallbacks } from 'uniswap/src/features/search/SearchModal/hooks/useFilterCallbacks' import { SearchTextInput } from 'uniswap/src/features/search/SearchTextInput' diff --git a/packages/uniswap/src/components/TokenSelector/TokenSelectorList.tsx b/packages/uniswap/src/components/TokenSelector/TokenSelectorList.tsx index 239da7655aa..b3c62a99d83 100644 --- a/packages/uniswap/src/components/TokenSelector/TokenSelectorList.tsx +++ b/packages/uniswap/src/components/TokenSelector/TokenSelectorList.tsx @@ -2,7 +2,6 @@ import { memo, useCallback, useState } from 'react' import { useDispatch } from 'react-redux' import { Text } from 'ui/src' import { BridgedAssetModal } from 'uniswap/src/components/BridgedAsset/BridgedAssetModal' -import { checkIsBridgedAsset } from 'uniswap/src/components/BridgedAsset/utils' import { TokenOptionItem as BaseTokenOptionItem, TokenContextMenuVariant, @@ -80,7 +79,7 @@ const TokenOptionItem = memo(function _TokenOptionItem({ const shouldShowWarningModalOnPress = showWarnings && (isBlocked || (severity !== WarningSeverity.None && !tokenWarningDismissed)) - const isBridgedAsset = checkIsBridgedAsset(currencyInfo) + const isBridgedAsset = Boolean(currencyInfo.isBridged) const [showBridgedAssetWarningModal, setShowBridgedAssetWarningModal] = useState(false) const { tokenWarningDismissed: bridgedAssetTokenWarningDismissed } = useDismissedBridgedAssetWarnings( currencyInfo.currency, diff --git a/packages/uniswap/src/components/TokenSelector/hooks.test.ts b/packages/uniswap/src/components/TokenSelector/hooks.test.ts index 274e9417792..7970d994ec6 100644 --- a/packages/uniswap/src/components/TokenSelector/hooks.test.ts +++ b/packages/uniswap/src/components/TokenSelector/hooks.test.ts @@ -62,6 +62,16 @@ jest.mock('uniswap/src/data/rest/tokenRankings', () => ({ tokenRankingsStatToCurrencyInfo: jest.fn(), })) +// Helper to convert undefined to null for GraphQL compatibility +const convertUndefinedToNull = ( + items: T[], +): T[] => + items.map((item) => ({ + ...item, + isBridged: item.isBridged ?? null, + bridgedWithdrawalInfo: item.bridgedWithdrawalInfo ?? null, + })) + const mockPortfolioHook = jest.requireMock( 'uniswap/src/components/TokenSelector/hooks/usePortfolioBalancesForAddressById', ) @@ -157,12 +167,12 @@ describe(useAllCommonBaseCurrencies, () => { { test: 'returns all currencies when there is no currency with a bridged version on other networks', input: projects, - output: { data: tokenProjectToCurrencyInfos(projects) }, + output: { data: convertUndefinedToNull(tokenProjectToCurrencyInfos(projects)) }, }, { test: 'filters out currencies that have a bridged version on other networks', input: [projectWithBridged], - output: { data: tokenProjectToCurrencyInfos([tokenProjectWithoutBridged]) }, + output: { data: convertUndefinedToNull(tokenProjectToCurrencyInfos([tokenProjectWithoutBridged])) }, }, ] @@ -215,7 +225,7 @@ describe(useFavoriteCurrencies, () => { { test: 'returns favorite tokens when there is data', input: [project], - output: { data: tokenProjectToCurrencyInfos([projectWithFavoritesOnly]) }, + output: { data: convertUndefinedToNull(tokenProjectToCurrencyInfos([projectWithFavoritesOnly])) }, }, ] @@ -319,22 +329,22 @@ describe(useFilterCallbacks, () => { expect(result.current.chainFilter).toEqual(UniverseChainId.ArbitrumOne) expect(result.current.searchFilter).toEqual('base uni') expect(result.current.parsedSearchFilter).toEqual(null) - expect(result.current.parsedSearchFilter).toEqual(null) }) it('does not parse unsupported chains', async () => { + const searchText = 'UNSUPPORTED uni' const { result } = renderHook(() => useFilterCallbacks(null, ModalName.Swap)) expect(result.current.parsedSearchFilter).toEqual(null) await act(() => { - result.current.onChangeText('UNSUPPORTED uni') + result.current.onChangeText(searchText) }) expect(result.current.chainFilter).toEqual(null) - expect(result.current.searchFilter).toEqual('UNSUPPORTED uni') + expect(result.current.searchFilter).toEqual(searchText) expect(result.current.parsedChainFilter).toEqual(null) - expect(result.current.parsedSearchFilter).toEqual(null) + expect(result.current.parsedSearchFilter).toEqual(searchText) }) it('only parses after the first space', async () => { @@ -399,17 +409,18 @@ describe(useFilterCallbacks, () => { it('does not parse unsupported chains from end', async () => { const { result } = renderHook(() => useFilterCallbacks(null, ModalName.Swap)) + const searchText = 'uni UNSUPPORTED' expect(result.current.parsedSearchFilter).toEqual(null) await act(() => { - result.current.onChangeText('uni UNSUPPORTED') + result.current.onChangeText(searchText) }) expect(result.current.chainFilter).toEqual(null) - expect(result.current.searchFilter).toEqual('uni UNSUPPORTED') + expect(result.current.searchFilter).toEqual(searchText) expect(result.current.parsedChainFilter).toEqual(null) - expect(result.current.parsedSearchFilter).toEqual(null) + expect(result.current.parsedSearchFilter).toEqual(searchText) }) }) diff --git a/packages/uniswap/src/components/activity/details/TransactionDetailsModal.test.tsx b/packages/uniswap/src/components/activity/details/TransactionDetailsModal.test.tsx index ab3a2ad969a..7aa920b47a9 100644 --- a/packages/uniswap/src/components/activity/details/TransactionDetailsModal.test.tsx +++ b/packages/uniswap/src/components/activity/details/TransactionDetailsModal.test.tsx @@ -83,7 +83,8 @@ jest.mock('uniswap/src/features/tokens/useCurrencyInfo', () => ({ }, })) -jest.mock('uniswap/src/features/gating/hooks', () => ({ +jest.mock('@universe/gating', () => ({ + ...jest.requireActual('@universe/gating'), useDynamicConfigValue: jest .fn() .mockImplementation(({ defaultValue }: { config: unknown; key: unknown; defaultValue: unknown }) => { diff --git a/packages/uniswap/src/components/activity/details/transactions/SwapTransactionDetails.test.tsx b/packages/uniswap/src/components/activity/details/transactions/SwapTransactionDetails.test.tsx index 5b4154799d9..ac166a53b89 100644 --- a/packages/uniswap/src/components/activity/details/transactions/SwapTransactionDetails.test.tsx +++ b/packages/uniswap/src/components/activity/details/transactions/SwapTransactionDetails.test.tsx @@ -51,7 +51,8 @@ jest.mock('uniswap/src/features/tokens/useCurrencyInfo', () => ({ }, })) -jest.mock('uniswap/src/features/gating/hooks', () => ({ +jest.mock('@universe/gating', () => ({ + ...jest.requireActual('@universe/gating'), useDynamicConfigValue: jest .fn() .mockImplementation(({ defaultValue }: { config: unknown; key: unknown; defaultValue: unknown }) => { diff --git a/packages/uniswap/src/components/activity/details/transactions/TransferTransactionDetails.test.tsx b/packages/uniswap/src/components/activity/details/transactions/TransferTransactionDetails.test.tsx index 6827f2805c4..52cfaa09a99 100644 --- a/packages/uniswap/src/components/activity/details/transactions/TransferTransactionDetails.test.tsx +++ b/packages/uniswap/src/components/activity/details/transactions/TransferTransactionDetails.test.tsx @@ -67,7 +67,8 @@ const getCurrencyInfoForChain = (chainId: number): CurrencyInfo => { } } -jest.mock('uniswap/src/features/gating/hooks', () => ({ +jest.mock('@universe/gating', () => ({ + ...jest.requireActual('@universe/gating'), useDynamicConfigValue: jest .fn() .mockImplementation(({ defaultValue }: { config: unknown; key: unknown; defaultValue: unknown }) => { diff --git a/packages/uniswap/src/components/gating/DynamicConfigDropdown.tsx b/packages/uniswap/src/components/gating/DynamicConfigDropdown.tsx index 17d4e3450cb..c3797f2dbd4 100644 --- a/packages/uniswap/src/components/gating/DynamicConfigDropdown.tsx +++ b/packages/uniswap/src/components/gating/DynamicConfigDropdown.tsx @@ -1,8 +1,6 @@ +import { DynamicConfigKeys, DynamicConfigs, getOverrideAdapter, useDynamicConfigValue } from '@universe/gating' import { Flex, Text } from 'ui/src' import { ActionSheetDropdown } from 'uniswap/src/components/dropdowns/ActionSheetDropdown' -import { DynamicConfigKeys, DynamicConfigs } from 'uniswap/src/features/gating/configs' -import { useDynamicConfigValue } from 'uniswap/src/features/gating/hooks' -import { getOverrideAdapter } from 'uniswap/src/features/gating/sdk/statsig' export function DynamicConfigDropdown({ config, diff --git a/packages/uniswap/src/components/gating/GatingOverrides.tsx b/packages/uniswap/src/components/gating/GatingOverrides.tsx index 4d82f77b9b2..94bae18eb5a 100644 --- a/packages/uniswap/src/components/gating/GatingOverrides.tsx +++ b/packages/uniswap/src/components/gating/GatingOverrides.tsx @@ -1,3 +1,16 @@ +import { + DynamicConfigs, + EmbeddedWalletConfigKey, + Experiments, + ExtensionBiometricUnlockConfigKey, + FeatureFlags, + ForceUpgradeConfigKey, + getFeatureFlagName, + getOverrideAdapter, + Layers, + useFeatureFlagWithExposureLoggingDisabled, + WALLET_FEATURE_FLAG_NAMES, +} from '@universe/gating' import React, { PropsWithChildren, useCallback } from 'react' import { Accordion, Flex, Separator, Switch, Text } from 'ui/src' import { RotatableChevron } from 'ui/src/components/icons/RotatableChevron' @@ -14,16 +27,6 @@ import { GatingButton } from 'uniswap/src/components/gating/GatingButton' import { ExperimentRow, LayerRow } from 'uniswap/src/components/gating/Rows' import { useForceUpgradeStatus } from 'uniswap/src/features/forceUpgrade/hooks/useForceUpgradeStatus' import { useForceUpgradeTranslations } from 'uniswap/src/features/forceUpgrade/hooks/useForceUpgradeTranslations' -import { - DynamicConfigs, - EmbeddedWalletConfigKey, - ExtensionBiometricUnlockConfigKey, - ForceUpgradeConfigKey, -} from 'uniswap/src/features/gating/configs' -import { Experiments, Layers } from 'uniswap/src/features/gating/experiments' -import { FeatureFlags, getFeatureFlagName, WALLET_FEATURE_FLAG_NAMES } from 'uniswap/src/features/gating/flags' -import { useFeatureFlagWithExposureLoggingDisabled } from 'uniswap/src/features/gating/hooks' -import { getOverrideAdapter } from 'uniswap/src/features/gating/sdk/statsig' import { useEmbeddedWalletBaseUrl } from 'uniswap/src/features/passkey/hooks/useEmbeddedWalletBaseUrl' import { isExtensionApp, isMobileApp } from 'utilities/src/platform' import { useEvent } from 'utilities/src/react/hooks' diff --git a/packages/uniswap/src/components/gating/Rows.tsx b/packages/uniswap/src/components/gating/Rows.tsx index c5797d1a13e..fac0346bbaf 100644 --- a/packages/uniswap/src/components/gating/Rows.tsx +++ b/packages/uniswap/src/components/gating/Rows.tsx @@ -1,7 +1,6 @@ +import { Experiments, getOverrideAdapter, LayerProperties, Layers, useExperiment, useLayer } from '@universe/gating' import { useCallback } from 'react' import { Flex, Input, Switch, Text } from 'ui/src' -import { Experiments, LayerProperties, Layers } from 'uniswap/src/features/gating/experiments' -import { getOverrideAdapter, useExperiment, useLayer } from 'uniswap/src/features/gating/sdk/statsig' export function LayerRow({ value: layerName, diff --git a/packages/uniswap/src/components/gating/dynamicConfigOverrides.tsx b/packages/uniswap/src/components/gating/dynamicConfigOverrides.tsx index a7ac74e9b63..166f0afd754 100644 --- a/packages/uniswap/src/components/gating/dynamicConfigOverrides.tsx +++ b/packages/uniswap/src/components/gating/dynamicConfigOverrides.tsx @@ -1,6 +1,6 @@ +import { ForceUpgradeStatus, ForceUpgradeTranslations } from '@universe/gating' import { ComponentProps } from 'react' import { DynamicConfigDropdown } from 'uniswap/src/components/gating/DynamicConfigDropdown' -import { ForceUpgradeStatus, ForceUpgradeTranslations } from 'uniswap/src/features/gating/configs' type DynamicConfigOptions = ComponentProps['options'] diff --git a/packages/uniswap/src/components/lists/items/pools/PoolOptionItem.tsx b/packages/uniswap/src/components/lists/items/pools/PoolOptionItem.tsx index 48a9871d69c..998c2f3630f 100644 --- a/packages/uniswap/src/components/lists/items/pools/PoolOptionItem.tsx +++ b/packages/uniswap/src/components/lists/items/pools/PoolOptionItem.tsx @@ -1,4 +1,4 @@ -import { ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' +import { ProtocolVersion } from '@uniswap/client-data-api/dist/data/v1/poolTypes_pb' import { memo } from 'react' import { Flex, Text } from 'ui/src' import { iconSizes } from 'ui/src/theme' diff --git a/packages/uniswap/src/components/lists/items/pools/PoolOptionItemContextMenu.tsx b/packages/uniswap/src/components/lists/items/pools/PoolOptionItemContextMenu.tsx index a6971c2edca..e2bd8a72534 100644 --- a/packages/uniswap/src/components/lists/items/pools/PoolOptionItemContextMenu.tsx +++ b/packages/uniswap/src/components/lists/items/pools/PoolOptionItemContextMenu.tsx @@ -1,4 +1,4 @@ -import { ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' +import { ProtocolVersion } from '@uniswap/client-data-api/dist/data/v1/poolTypes_pb' import React, { ReactNode, useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { CheckCircleFilled } from 'ui/src/components/icons/CheckCircleFilled' diff --git a/packages/uniswap/src/components/lists/items/pools/usePoolSearchResultsToPoolOptions.tsx b/packages/uniswap/src/components/lists/items/pools/usePoolSearchResultsToPoolOptions.tsx index d1d48af9eb7..146cefe7f3d 100644 --- a/packages/uniswap/src/components/lists/items/pools/usePoolSearchResultsToPoolOptions.tsx +++ b/packages/uniswap/src/components/lists/items/pools/usePoolSearchResultsToPoolOptions.tsx @@ -1,4 +1,4 @@ -import { ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' +import { ProtocolVersion } from '@uniswap/client-data-api/dist/data/v1/poolTypes_pb' import { useMemo } from 'react' import { OnchainItemListOptionType, PoolOption } from 'uniswap/src/components/lists/items/types' import { ZERO_ADDRESS } from 'uniswap/src/constants/misc' diff --git a/packages/uniswap/src/components/lists/items/pools/usePoolStatsToPoolOptions.tsx b/packages/uniswap/src/components/lists/items/pools/usePoolStatsToPoolOptions.tsx index fcdd6b49718..715546ef5be 100644 --- a/packages/uniswap/src/components/lists/items/pools/usePoolStatsToPoolOptions.tsx +++ b/packages/uniswap/src/components/lists/items/pools/usePoolStatsToPoolOptions.tsx @@ -1,5 +1,5 @@ +import { ProtocolVersion } from '@uniswap/client-data-api/dist/data/v1/poolTypes_pb' import { PoolStats } from '@uniswap/client-explore/dist/uniswap/explore/v1/service_pb' -import { ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' import { parseRestProtocolVersion } from '@universe/api' import { useMemo } from 'react' import { OnchainItemListOptionType, PoolOption } from 'uniswap/src/components/lists/items/types' diff --git a/packages/uniswap/src/components/lists/items/types.ts b/packages/uniswap/src/components/lists/items/types.ts index 0944d974245..2ab0ff8035b 100644 --- a/packages/uniswap/src/components/lists/items/types.ts +++ b/packages/uniswap/src/components/lists/items/types.ts @@ -1,4 +1,4 @@ -import { ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' +import { ProtocolVersion } from '@uniswap/client-data-api/dist/data/v1/poolTypes_pb' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' diff --git a/packages/uniswap/src/components/nfts/NftView.tsx b/packages/uniswap/src/components/nfts/NftView.tsx index b9f9dffbae8..9c57a605347 100644 --- a/packages/uniswap/src/components/nfts/NftView.tsx +++ b/packages/uniswap/src/components/nfts/NftView.tsx @@ -10,9 +10,10 @@ export type NftViewProps = { onPress: () => void walletAddresses: Address[] openContextMenu?: () => void + hoverAnimation?: boolean } -export function NftView({ item, onPress, index, openContextMenu }: NftViewProps): JSX.Element { +export function NftView({ item, onPress, index, openContextMenu, hoverAnimation = true }: NftViewProps): JSX.Element { const nftView = ( skip?: boolean customEmptyState?: JSX.Element + autoColumns?: boolean + /** Web-only: when true, use a flex-wrap container instead of 2-col grid */ + wrapFlex?: boolean }, 'renderItem' | 'data' -> +> & { + loadingSkeletonCount?: number +} export function NftsList(_props: NftsListProps): JSX.Element { throw new PlatformSplitStubError('NftsList') diff --git a/packages/uniswap/src/components/nfts/NftsList.web.tsx b/packages/uniswap/src/components/nfts/NftsList.web.tsx index 5d957a39e0d..1e92a24062b 100644 --- a/packages/uniswap/src/components/nfts/NftsList.web.tsx +++ b/packages/uniswap/src/components/nfts/NftsList.web.tsx @@ -2,7 +2,7 @@ import { isNonPollingRequestInFlight } from '@universe/api' import { useCallback, useEffect, useMemo } from 'react' import { useTranslation } from 'react-i18next' import InfiniteScroll from 'react-infinite-scroll-component' -import { Flex, Loader, View } from 'ui/src' +import { Flex, Loader, styled, View } from 'ui/src' import { NoNfts } from 'ui/src/components/icons/NoNfts' import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard' import { ExpandoRow } from 'uniswap/src/components/ExpandoRow/ExpandoRow' @@ -17,24 +17,29 @@ import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { isExtensionApp } from 'utilities/src/platform' -const AssetsContainer = ({ children, useGrid }: { children: React.ReactNode; useGrid: boolean }): JSX.Element => { - return ( - - {children} - - ) -} +const AssetsContainer = styled(View, { + width: '100%', + gap: '$spacing2', + variants: { + useGrid: { + true: { + '$platform-web': { + display: 'grid', + // default to 2 columns + gridTemplateColumns: 'minmax(0, 1fr) minmax(0, 1fr)', + gridGap: '12px', + }, + }, + }, + autoColumns: { + true: { + '$platform-web': { + gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))', + }, + }, + }, + }, +}) const LOADING_ITEM = 'loading' @@ -48,6 +53,8 @@ export function NftsList({ renderNFTItem, skip, customEmptyState, + autoColumns = false, + loadingSkeletonCount = 6, }: NftsListProps): JSX.Element { const { t } = useTranslation() @@ -96,7 +103,7 @@ export function NftsList({ return null case HIDDEN_NFTS_ROW: return ( - + ( <> - - - - - - - - - - + {Array.from({ length: loadingSkeletonCount }, (_, i) => ( + + ))} ), - [], + [loadingSkeletonCount], ) const emptyState = useMemo( @@ -197,7 +197,9 @@ export function NftsList({ style={{ overflow: 'unset' }} scrollableTarget="wallet-dropdown-scroll-wrapper" > - 0}>{listContent} + 0} autoColumns={autoColumns}> + {listContent} + ) diff --git a/packages/uniswap/src/components/notifications/NotificationToast.native.tsx b/packages/uniswap/src/components/notifications/NotificationToast.native.tsx index b97222b2910..fd0a2c1a101 100644 --- a/packages/uniswap/src/components/notifications/NotificationToast.native.tsx +++ b/packages/uniswap/src/components/notifications/NotificationToast.native.tsx @@ -39,11 +39,11 @@ export function NotificationToast({ const onDismissLatest = useCallback(() => { bannerOffset.value = withSpring(HIDE_OFFSET_Y, SPRING_ANIMATION) - }, [bannerOffset]) + }, []) const onShowCurrentNotification = useCallback(() => { bannerOffset.value = withDelay(SPRING_ANIMATION_DELAY, withSpring(showOffset, SPRING_ANIMATION)) - }, [bannerOffset, showOffset]) + }, [showOffset]) const { onActionButtonPress, onNotificationPress, cancelDismiss, dismissLatest } = useNotificationLifecycle({ actionButtonOnPress: actionButton?.onPress, diff --git a/packages/uniswap/src/data/apiClients/tradingApi/TradingApiClient.ts b/packages/uniswap/src/data/apiClients/tradingApi/TradingApiClient.ts index 3a5211d76a3..433155b730f 100644 --- a/packages/uniswap/src/data/apiClients/tradingApi/TradingApiClient.ts +++ b/packages/uniswap/src/data/apiClients/tradingApi/TradingApiClient.ts @@ -1,10 +1,9 @@ import { createTradingApiClient, TradingApi } from '@universe/api' +import { FeatureFlags, getFeatureFlag } from '@universe/gating' import { config } from 'uniswap/src/config' import { tradingApiVersionPrefix, uniswapUrls } from 'uniswap/src/constants/urls' import { createUniswapFetchClient } from 'uniswap/src/data/apiClients/createUniswapFetchClient' import { filterChainIdsByPlatform } from 'uniswap/src/features/chains/utils' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { getFeatureFlag } from 'uniswap/src/features/gating/hooks' import { Platform } from 'uniswap/src/features/platforms/types/Platform' const TradingFetchClient = createUniswapFetchClient({ diff --git a/packages/uniswap/src/data/apiClients/uniswapApi/useGasFeeQuery.ts b/packages/uniswap/src/data/apiClients/uniswapApi/useGasFeeQuery.ts index a1bcecfc55c..b2ab75b8cf7 100644 --- a/packages/uniswap/src/data/apiClients/uniswapApi/useGasFeeQuery.ts +++ b/packages/uniswap/src/data/apiClients/uniswapApi/useGasFeeQuery.ts @@ -4,13 +4,13 @@ import { type UseQueryWithImmediateGarbageCollectionApiHelperHookArgs, useQueryWithImmediateGarbageCollection, } from '@universe/api' +import { useStatsigClientStatus } from '@universe/gating' import { uniswapUrls } from 'uniswap/src/constants/urls' import { createFetchGasFee, type GasFeeResultWithoutState, } from 'uniswap/src/data/apiClients/uniswapApi/UniswapApiClient' import { getActiveGasStrategy } from 'uniswap/src/features/gas/utils' -import { useStatsigClientStatus } from 'uniswap/src/features/gating/hooks' import { ReactQueryCacheKey } from 'utilities/src/reactQuery/cache' export function useGasFeeQuery({ diff --git a/packages/uniswap/src/data/rest/conversionTracking/useConversionProxy.ts b/packages/uniswap/src/data/rest/conversionTracking/useConversionProxy.ts index 87391cd4998..af8c28f62ae 100644 --- a/packages/uniswap/src/data/rest/conversionTracking/useConversionProxy.ts +++ b/packages/uniswap/src/data/rest/conversionTracking/useConversionProxy.ts @@ -3,9 +3,8 @@ import { type ConnectError, type Transport } from '@connectrpc/connect' import { useMutation } from '@connectrpc/connect-query' import { type UseMutationResult } from '@tanstack/react-query' import { ConversionTrackingApi, createConnectTransportWithDefaults } from '@universe/api' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { getConversionProxyApiBaseUrl } from 'uniswap/src/data/rest/conversionTracking/utils' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' const createConversionProxyTransport = (isConversionApiMigrationEnabled: boolean): Transport => createConnectTransportWithDefaults({ diff --git a/packages/uniswap/src/data/rest/conversionTracking/useConversionTracking.ts b/packages/uniswap/src/data/rest/conversionTracking/useConversionTracking.ts index b64191593ff..c9307f17d9a 100644 --- a/packages/uniswap/src/data/rest/conversionTracking/useConversionTracking.ts +++ b/packages/uniswap/src/data/rest/conversionTracking/useConversionTracking.ts @@ -1,4 +1,5 @@ import { ConnectError } from '@connectrpc/connect' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { useAtom } from 'jotai' import { atomWithStorage } from 'jotai/utils' import { parse } from 'qs' @@ -12,8 +13,6 @@ import { buildProxyRequest } from 'uniswap/src/data/rest/conversionTracking/trac import { ConversionLead, PlatformIdType, TrackConversionArgs } from 'uniswap/src/data/rest/conversionTracking/types' import { useConversionProxy } from 'uniswap/src/data/rest/conversionTracking/useConversionProxy' import { getExternalConversionLeadsCookie } from 'uniswap/src/data/rest/conversionTracking/utils' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { UniswapEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { HexString } from 'utilities/src/addresses/hex' diff --git a/packages/uniswap/src/data/rest/getPair.ts b/packages/uniswap/src/data/rest/getPair.ts deleted file mode 100644 index b108414c5d7..00000000000 --- a/packages/uniswap/src/data/rest/getPair.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { PartialMessage } from '@bufbuild/protobuf' -import { ConnectError } from '@connectrpc/connect' -import { useQuery } from '@connectrpc/connect-query' -import { UseQueryResult } from '@tanstack/react-query' -import { GetPairRequest, GetPairResponse } from '@uniswap/client-pools/dist/pools/v1/api_pb' -import { getPair } from '@uniswap/client-pools/dist/pools/v1/api-PoolsService_connectquery' -import { uniswapGetTransport } from 'uniswap/src/data/rest/base' - -/** - * eslint-disable import/no-unused-modules -- this endpoint is returning stale data sometimes meaning - * that the data we get (i.e. the dependent amount) is incorrect and the transaction does not complete on chain. - * Use this endpoint again once the data is more up to date or the trading API handles the data discrepancy. - */ -export function useGetPair( - input?: PartialMessage, - enabled = true, -): UseQueryResult { - return useQuery(getPair, input, { transport: uniswapGetTransport, enabled, retry: false }) -} diff --git a/packages/uniswap/src/data/rest/getPools.ts b/packages/uniswap/src/data/rest/getPools.ts index 106bca1c926..1dcc0a98ead 100644 --- a/packages/uniswap/src/data/rest/getPools.ts +++ b/packages/uniswap/src/data/rest/getPools.ts @@ -2,8 +2,8 @@ import { PartialMessage } from '@bufbuild/protobuf' import { ConnectError } from '@connectrpc/connect' import { useQuery } from '@connectrpc/connect-query' import { UseQueryResult } from '@tanstack/react-query' -import { ListPoolsRequest, ListPoolsResponse } from '@uniswap/client-pools/dist/pools/v1/api_pb' -import { listPools } from '@uniswap/client-pools/dist/pools/v1/api-PoolsService_connectquery' +import { ListPoolsRequest, ListPoolsResponse } from '@uniswap/client-data-api/dist/data/v1/api_pb' +import { listPools } from '@uniswap/client-data-api/dist/data/v1/api-DataApiService_connectquery' import { uniswapGetTransport } from 'uniswap/src/data/rest/base' export function useGetPoolsByTokens( diff --git a/packages/uniswap/src/data/rest/getPoolsRewards.ts b/packages/uniswap/src/data/rest/getPoolsRewards.ts index 58a5abfed60..8d681b88749 100644 --- a/packages/uniswap/src/data/rest/getPoolsRewards.ts +++ b/packages/uniswap/src/data/rest/getPoolsRewards.ts @@ -2,8 +2,8 @@ import { PartialMessage } from '@bufbuild/protobuf' import { ConnectError } from '@connectrpc/connect' import { useQuery } from '@connectrpc/connect-query' import { UseQueryResult } from '@tanstack/react-query' -import { GetRewardsRequest, GetRewardsResponse } from '@uniswap/client-pools/dist/pools/v1/api_pb' -import { getRewards } from '@uniswap/client-pools/dist/pools/v1/api-PoolsService_connectquery' +import { GetRewardsRequest, GetRewardsResponse } from '@uniswap/client-data-api/dist/data/v1/api_pb' +import { getRewards } from '@uniswap/client-data-api/dist/data/v1/api-DataApiService_connectquery' import { uniswapGetTransport } from 'uniswap/src/data/rest/base' export function useGetPoolsRewards( diff --git a/packages/uniswap/src/data/rest/getPortfolio.ts b/packages/uniswap/src/data/rest/getPortfolio.ts index 5db0dd4021d..3b3db8f6bb3 100644 --- a/packages/uniswap/src/data/rest/getPortfolio.ts +++ b/packages/uniswap/src/data/rest/getPortfolio.ts @@ -15,7 +15,6 @@ import { cleanupCaughtUpOverrides, getOverridesForAddress, getOverridesForQuery, - getPortfolioQueryApolloClient, getPortfolioQueryReduxStore, } from 'uniswap/src/data/rest/portfolioBalanceOverrides' import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' @@ -107,9 +106,8 @@ export const getPortfolioQuery = ({ try { const reduxStore = getPortfolioQueryReduxStore() - const apolloClient = getPortfolioQueryApolloClient() - if (!reduxStore || !apolloClient) { + if (!reduxStore) { log.warn('`getPortfolioQuery` called before `initializePortfolioQueryOverrides`') return apiResponse } @@ -150,7 +148,6 @@ export const getPortfolioQuery = ({ }) const mergedResult = await fetchAndMergeOnchainBalances({ - apolloClient, cachedPortfolio: modifiedResponse.portfolio, accountAddress: address, currencyIds: overridesForCurrentAddress, diff --git a/packages/uniswap/src/data/rest/getPosition.ts b/packages/uniswap/src/data/rest/getPosition.ts index 857c7d5a83c..22218d7f21a 100644 --- a/packages/uniswap/src/data/rest/getPosition.ts +++ b/packages/uniswap/src/data/rest/getPosition.ts @@ -2,8 +2,8 @@ import { PartialMessage } from '@bufbuild/protobuf' import { ConnectError } from '@connectrpc/connect' import { useQuery } from '@connectrpc/connect-query' import { UseQueryResult } from '@tanstack/react-query' -import { GetPositionRequest, GetPositionResponse } from '@uniswap/client-pools/dist/pools/v1/api_pb' -import { getPosition } from '@uniswap/client-pools/dist/pools/v1/api-PoolsService_connectquery' +import { GetPositionRequest, GetPositionResponse } from '@uniswap/client-data-api/dist/data/v1/api_pb' +import { getPosition } from '@uniswap/client-data-api/dist/data/v1/api-DataApiService_connectquery' import { uniswapPostTransport } from 'uniswap/src/data/rest/base' export function useGetPositionQuery( diff --git a/packages/uniswap/src/data/rest/getPositions.ts b/packages/uniswap/src/data/rest/getPositions.ts index 0a5431262e6..400b3115f92 100644 --- a/packages/uniswap/src/data/rest/getPositions.ts +++ b/packages/uniswap/src/data/rest/getPositions.ts @@ -12,9 +12,9 @@ import { GetPositionResponse, ListPositionsRequest, ListPositionsResponse, -} from '@uniswap/client-pools/dist/pools/v1/api_pb' -import { getPosition, listPositions } from '@uniswap/client-pools/dist/pools/v1/api-PoolsService_connectquery' -import { ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' +} from '@uniswap/client-data-api/dist/data/v1/api_pb' +import { getPosition, listPositions } from '@uniswap/client-data-api/dist/data/v1/api-DataApiService_connectquery' +import { ProtocolVersion } from '@uniswap/client-data-api/dist/data/v1/poolTypes_pb' import { Pair } from '@uniswap/v2-sdk' import { useMemo } from 'react' import { uniswapPostTransport } from 'uniswap/src/data/rest/base' diff --git a/packages/uniswap/src/data/rest/portfolioBalanceOverrides.ts b/packages/uniswap/src/data/rest/portfolioBalanceOverrides.ts index 695e5229893..24badbde9bc 100644 --- a/packages/uniswap/src/data/rest/portfolioBalanceOverrides.ts +++ b/packages/uniswap/src/data/rest/portfolioBalanceOverrides.ts @@ -1,4 +1,3 @@ -import { ApolloClient, NormalizedCacheObject } from '@apollo/client' import { ToolkitStore } from '@reduxjs/toolkit/dist/configureStore' import { GetPortfolioResponse } from '@uniswap/client-data-api/dist/data/v1/api_pb' import { getNativeAddress } from 'uniswap/src/constants/addresses' @@ -16,30 +15,22 @@ const FILE_NAME = 'portfolioBalanceOverrides.ts' // so instead of checking for exact equality, we check if the quantities are "aproximately" equal. const APPROXIMATE_EQUALITY_THRESHOLD_PERCENT = 0.02 // 2% -// Module-level references to Redux store and Apollo client +// Module-level references to Redux store // These are initialized once during app startup let portfolioQueryReduxStore: ToolkitStore | null = null -let portfolioQueryApolloClient: ApolloClient | null = null /** * Initializes the portfolio balance override mechanism. - * This must be called once during each app initialization after both the Redux store and Apollo client are created. + * This must be called once during each app initialization after the Redux store is created. */ -export function initializePortfolioQueryOverrides({ - store, - apolloClient, -}: { - store: ToolkitStore - apolloClient: ApolloClient -}): void { +export function initializePortfolioQueryOverrides({ store }: { store: ToolkitStore }): void { const log = createLogger(FILE_NAME, 'initializePortfolioQueryOverrides', '[REST-ITBU]') - if (portfolioQueryReduxStore || portfolioQueryApolloClient) { + if (portfolioQueryReduxStore) { log.warn('`initializePortfolioQueryOverrides` called multiple times') } portfolioQueryReduxStore = store - portfolioQueryApolloClient = apolloClient log.debug('Portfolio query overrides successfully initialized') } @@ -48,10 +39,6 @@ export function getPortfolioQueryReduxStore(): ToolkitStore | null { return portfolioQueryReduxStore } -export function getPortfolioQueryApolloClient(): ApolloClient | null { - return portfolioQueryApolloClient -} - const selectTokenBalanceOverridesForWalletAddress = makeSelectTokenBalanceOverridesForWalletAddress() /** diff --git a/packages/uniswap/src/data/rest/searchTokensAndPools.ts b/packages/uniswap/src/data/rest/searchTokensAndPools.ts index 47fcae4534a..0657cb1681c 100644 --- a/packages/uniswap/src/data/rest/searchTokensAndPools.ts +++ b/packages/uniswap/src/data/rest/searchTokensAndPools.ts @@ -1,22 +1,29 @@ import { PartialMessage } from '@bufbuild/protobuf' import { ConnectError } from '@connectrpc/connect' -import { useQuery } from '@connectrpc/connect-query' +import { createQueryOptions, useQuery } from '@connectrpc/connect-query' import { UseQueryResult } from '@tanstack/react-query' import { Pool, type Token as SearchToken, SearchTokensRequest, SearchTokensResponse, + SearchType, } from '@uniswap/client-search/dist/search/v1/api_pb' import { searchTokens } from '@uniswap/client-search/dist/search/v1/api-searchService_connectquery' -import { parseProtectionInfo, parseRestProtocolVersion, parseSafetyLevel } from '@universe/api' +import { parseProtectionInfo, parseRestProtocolVersion, parseSafetyLevel, SharedQueryClient } from '@universe/api' import { getNativeAddress } from 'uniswap/src/constants/addresses' import { uniswapPostTransport } from 'uniswap/src/data/rest/base' +import { createLogger } from 'utilities/src/logger/logger' + +const FILE_NAME = 'searchTokensAndPools.ts' + +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' import { buildCurrency, buildCurrencyInfo } from 'uniswap/src/features/dataApi/utils/buildCurrency' import { getCurrencySafetyInfo } from 'uniswap/src/features/dataApi/utils/getCurrencySafetyInfo' import { PoolSearchHistoryResult, SearchHistoryResultType } from 'uniswap/src/features/search/SearchHistoryResult' import { buildCurrencyId, currencyId, isNativeCurrencyAddress } from 'uniswap/src/utils/currencyId' +import { ONE_DAY_MS, ONE_HOUR_MS } from 'utilities/src/time/time' /** * Wrapper around Tanstack useQuery for the Uniswap REST BE service SearchTokens @@ -40,6 +47,56 @@ export function useSearchTokensAndPoolsQuery({ }) } +/** + * Fetch a single token by address outside of React components + * @param chainId - The chain ID to search on + * @param address - The token address to look up + * @returns Token data or null if not found + */ +export async function fetchTokenByAddress({ + chainId, + address, +}: { + chainId: UniverseChainId + address: string +}): Promise { + const log = createLogger(FILE_NAME, 'fetchTokenByAddress') + + try { + const result = await SharedQueryClient.fetchQuery({ + ...createQueryOptions( + searchTokens, + { + searchQuery: address, + chainIds: [chainId], + searchType: SearchType.TOKEN, + size: 1, + page: 1, + }, + { transport: uniswapPostTransport }, + ), + // Token data does not change often, so we can use stale data here. + // This data will be refreshed when fetching the portfolio balances anyway. + staleTime: ONE_HOUR_MS, + gcTime: ONE_DAY_MS, + }) + + const token = result.tokens[0] ?? null + + if (!token) { + log.debug('Token not found in search results', { chainId, address }) + } + + return token + } catch (error) { + log.error(error, { + chainId, + address, + }) + return null + } +} + export function searchTokenToCurrencyInfo(token: SearchToken): CurrencyInfo | null { const { chainId, address, symbol, name, decimals, logoUrl, feeData } = token const safetyLevel = parseSafetyLevel(token.safetyLevel) diff --git a/packages/uniswap/src/features/activity/formatTransactionsByDate.ts b/packages/uniswap/src/features/activity/formatTransactionsByDate.ts index 8283078091f..dff387c77ba 100644 --- a/packages/uniswap/src/features/activity/formatTransactionsByDate.ts +++ b/packages/uniswap/src/features/activity/formatTransactionsByDate.ts @@ -62,15 +62,30 @@ export function formatTransactionsByDate( // For all transactions before yesterday, group by month const priorByMonthTransactionList = olderThan24HTransactionList.reduce( (accum: Record, item) => { + // Skip transactions with invalid timestamps + if (!item.addedTime || item.addedTime <= 0) { + return accum + } + const isPreviousYear = item.addedTime < msTimestampCutoffYear - const key = localizedDayjs(item.addedTime) + const dayjsDate = localizedDayjs(item.addedTime) + const maybeKeyFromDayjsDate = dayjsDate // If in a previous year, append year to key string, else just use month // This key is used as the section title in TransactionList .format(isPreviousYear ? FORMAT_DATE_MONTH_YEAR : FORMAT_DATE_MONTH) .toString() - const currentMonthList = accum[key] ?? [] + + // Fallback to English if localized formatting fails + const validatedKey = dayjsDate.isValid() + ? maybeKeyFromDayjsDate + : dayjs(item.addedTime) + .locale('en') + .format(isPreviousYear ? FORMAT_DATE_MONTH_YEAR : FORMAT_DATE_MONTH) + + const currentMonthList = accum[validatedKey] ?? [] currentMonthList.push(item) - accum[key] = currentMonthList + accum[validatedKey] = currentMonthList + return accum }, {}, diff --git a/packages/uniswap/src/features/behaviorHistory/slice.ts b/packages/uniswap/src/features/behaviorHistory/slice.ts index 72c644fe19f..76c0e39b55b 100644 --- a/packages/uniswap/src/features/behaviorHistory/slice.ts +++ b/packages/uniswap/src/features/behaviorHistory/slice.ts @@ -9,6 +9,7 @@ export interface UniswapBehaviorHistoryState { hasDismissedBridgingWarning?: boolean hasDismissedLowNetworkTokenWarning?: boolean hasViewedContractAddressExplainer?: boolean + hasDismissedBridgedAssetsBannerV2?: boolean unichainPromotion?: { coldBannerDismissed?: boolean warmBannerDismissed?: boolean @@ -33,6 +34,7 @@ export const initialUniswapBehaviorHistoryState: UniswapBehaviorHistoryState = { hasDismissedBridgingWarning: false, hasDismissedLowNetworkTokenWarning: false, hasViewedContractAddressExplainer: false, + hasDismissedBridgedAssetsBannerV2: false, unichainPromotion: { coldBannerDismissed: false, warmBannerDismissed: false, @@ -108,6 +110,9 @@ const slice = createSlice({ setHasSeenToucanIntroModal: (state, action: PayloadAction) => { state.hasSeenToucanIntroModal = action.payload }, + setHasDismissedBridgedAssetsBannerV2: (state, action: PayloadAction) => { + state.hasDismissedBridgedAssetsBannerV2 = action.payload + }, }, }) @@ -128,6 +133,7 @@ export const { setEmbeddedWalletGraduateCardDismissed, setHasShownSmartWalletNudge, setHasSeenToucanIntroModal, + setHasDismissedBridgedAssetsBannerV2, } = slice.actions export const uniswapBehaviorHistoryReducer = slice.reducer diff --git a/packages/uniswap/src/features/chains/evm/info/avalanche.ts b/packages/uniswap/src/features/chains/evm/info/avalanche.ts index e7b8d376100..b439bc592c3 100644 --- a/packages/uniswap/src/features/chains/evm/info/avalanche.ts +++ b/packages/uniswap/src/features/chains/evm/info/avalanche.ts @@ -1,5 +1,6 @@ import { Token } from '@uniswap/sdk-core' import { GraphQLApi } from '@universe/api' +import { SwapConfigKey } from '@universe/gating' import { AVALANCHE_LOGO } from 'ui/src/assets' import { config } from 'uniswap/src/config' import { DEFAULT_NATIVE_ADDRESS_LEGACY, getQuicknodeEndpointUrl } from 'uniswap/src/features/chains/evm/rpc' @@ -11,7 +12,6 @@ import { UniverseChainId, UniverseChainInfo, } from 'uniswap/src/features/chains/types' -import { SwapConfigKey } from 'uniswap/src/features/gating/configs' import { Platform } from 'uniswap/src/features/platforms/types/Platform' import { ElementName } from 'uniswap/src/features/telemetry/constants' import { buildUSDC, buildUSDT } from 'uniswap/src/features/tokens/stablecoin' diff --git a/packages/uniswap/src/features/chains/evm/info/celo.ts b/packages/uniswap/src/features/chains/evm/info/celo.ts index c58fe928cae..d1fc7111b55 100644 --- a/packages/uniswap/src/features/chains/evm/info/celo.ts +++ b/packages/uniswap/src/features/chains/evm/info/celo.ts @@ -1,4 +1,5 @@ import { GraphQLApi } from '@universe/api' +import { SwapConfigKey } from '@universe/gating' import { CELO_LOGO } from 'ui/src/assets' import { config } from 'uniswap/src/config' import { getQuicknodeEndpointUrl } from 'uniswap/src/features/chains/evm/rpc' @@ -10,7 +11,6 @@ import { UniverseChainId, UniverseChainInfo, } from 'uniswap/src/features/chains/types' -import { SwapConfigKey } from 'uniswap/src/features/gating/configs' import { Platform } from 'uniswap/src/features/platforms/types/Platform' import { ElementName } from 'uniswap/src/features/telemetry/constants' import { buildUSDC } from 'uniswap/src/features/tokens/stablecoin' diff --git a/packages/uniswap/src/features/chains/evm/info/mainnet.ts b/packages/uniswap/src/features/chains/evm/info/mainnet.ts index 0283a360cec..374332ae025 100644 --- a/packages/uniswap/src/features/chains/evm/info/mainnet.ts +++ b/packages/uniswap/src/features/chains/evm/info/mainnet.ts @@ -1,5 +1,6 @@ import { CurrencyAmount } from '@uniswap/sdk-core' import { GraphQLApi } from '@universe/api' +import { SwapConfigKey } from '@universe/gating' import { ETH_LOGO, ETHEREUM_LOGO } from 'ui/src/assets' import { config } from 'uniswap/src/config' import { @@ -16,7 +17,6 @@ import { UniverseChainId, UniverseChainInfo, } from 'uniswap/src/features/chains/types' -import { SwapConfigKey } from 'uniswap/src/features/gating/configs' import { Platform } from 'uniswap/src/features/platforms/types/Platform' import { ElementName } from 'uniswap/src/features/telemetry/constants' import { buildDAI, buildUSDC, buildUSDT } from 'uniswap/src/features/tokens/stablecoin' diff --git a/packages/uniswap/src/features/chains/evm/info/monad.ts b/packages/uniswap/src/features/chains/evm/info/monad.ts index 7ea38b26778..740d57a0d05 100644 --- a/packages/uniswap/src/features/chains/evm/info/monad.ts +++ b/packages/uniswap/src/features/chains/evm/info/monad.ts @@ -1,4 +1,5 @@ import { GraphQLApi } from '@universe/api' +import { SwapConfigKey } from '@universe/gating' import { MONAD_LOGO } from 'ui/src/assets' import { DEFAULT_NATIVE_ADDRESS_LEGACY, getQuicknodeEndpointUrl } from 'uniswap/src/features/chains/evm/rpc' import { buildChainTokens } from 'uniswap/src/features/chains/evm/tokens' @@ -9,7 +10,6 @@ import { UniverseChainId, UniverseChainInfo, } from 'uniswap/src/features/chains/types' -import { SwapConfigKey } from 'uniswap/src/features/gating/configs' import { Platform } from 'uniswap/src/features/platforms/types/Platform' import { ElementName } from 'uniswap/src/features/telemetry/constants' import { buildUSDT } from 'uniswap/src/features/tokens/stablecoin' diff --git a/packages/uniswap/src/features/chains/evm/info/polygon.ts b/packages/uniswap/src/features/chains/evm/info/polygon.ts index 2624aded152..e13b25e96d9 100644 --- a/packages/uniswap/src/features/chains/evm/info/polygon.ts +++ b/packages/uniswap/src/features/chains/evm/info/polygon.ts @@ -1,4 +1,5 @@ import { GraphQLApi } from '@universe/api' +import { SwapConfigKey } from '@universe/gating' import { POLYGON_LOGO } from 'ui/src/assets' import { config } from 'uniswap/src/config' import { getQuicknodeEndpointUrl } from 'uniswap/src/features/chains/evm/rpc' @@ -10,7 +11,6 @@ import { UniverseChainId, UniverseChainInfo, } from 'uniswap/src/features/chains/types' -import { SwapConfigKey } from 'uniswap/src/features/gating/configs' import { Platform } from 'uniswap/src/features/platforms/types/Platform' import { ElementName } from 'uniswap/src/features/telemetry/constants' import { buildDAI, buildUSDC, buildUSDT } from 'uniswap/src/features/tokens/stablecoin' diff --git a/packages/uniswap/src/features/chains/gasDefaults.ts b/packages/uniswap/src/features/chains/gasDefaults.ts index 3ef71913717..7e5e8f0c8dd 100644 --- a/packages/uniswap/src/features/chains/gasDefaults.ts +++ b/packages/uniswap/src/features/chains/gasDefaults.ts @@ -1,4 +1,4 @@ -import { SwapConfigKey } from 'uniswap/src/features/gating/configs' +import { SwapConfigKey } from '@universe/gating' /** * Shared gas configuration constants. diff --git a/packages/uniswap/src/features/chains/hooks/useFeatureFlaggedChainIds.ts b/packages/uniswap/src/features/chains/hooks/useFeatureFlaggedChainIds.ts index 969e7995190..36dbf2723ea 100644 --- a/packages/uniswap/src/features/chains/hooks/useFeatureFlaggedChainIds.ts +++ b/packages/uniswap/src/features/chains/hooks/useFeatureFlaggedChainIds.ts @@ -1,8 +1,7 @@ +import { FeatureFlags, getFeatureFlag, useFeatureFlag } from '@universe/gating' import { useMemo } from 'react' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { filterChainIdsByFeatureFlag } from 'uniswap/src/features/chains/utils' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { getFeatureFlag, useFeatureFlag } from 'uniswap/src/features/gating/hooks' export const getFeatureFlaggedChainIds = createGetFeatureFlaggedChainIds({ getSoneiumStatus: () => getFeatureFlag(FeatureFlags.Soneium), diff --git a/packages/uniswap/src/features/chains/hooks/useNewChainIds.ts b/packages/uniswap/src/features/chains/hooks/useNewChainIds.ts index 8a9bd27b642..831f54704fc 100644 --- a/packages/uniswap/src/features/chains/hooks/useNewChainIds.ts +++ b/packages/uniswap/src/features/chains/hooks/useNewChainIds.ts @@ -1,8 +1,7 @@ +import { ChainsConfigKey, DynamicConfigs, useDynamicConfigValue } from '@universe/gating' import { useMemo } from 'react' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { isUniverseChainId } from 'uniswap/src/features/chains/utils' -import { ChainsConfigKey, DynamicConfigs } from 'uniswap/src/features/gating/configs' -import { useDynamicConfigValue } from 'uniswap/src/features/gating/hooks' import { isUniverseChainIdArrayType } from 'uniswap/src/features/gating/typeGuards' export function useNewChainIds(): UniverseChainId[] { diff --git a/packages/uniswap/src/features/chains/hooks/useOrderedChainIds.ts b/packages/uniswap/src/features/chains/hooks/useOrderedChainIds.ts index b74ac7d3e58..da0cbd451dd 100644 --- a/packages/uniswap/src/features/chains/hooks/useOrderedChainIds.ts +++ b/packages/uniswap/src/features/chains/hooks/useOrderedChainIds.ts @@ -1,8 +1,7 @@ +import { ChainsConfigKey, DynamicConfigs, useDynamicConfigValue } from '@universe/gating' import { useMemo } from 'react' import { ALL_CHAIN_IDS } from 'uniswap/src/features/chains/chainInfo' import { UniverseChainId } from 'uniswap/src/features/chains/types' -import { ChainsConfigKey, DynamicConfigs } from 'uniswap/src/features/gating/configs' -import { useDynamicConfigValue } from 'uniswap/src/features/gating/hooks' // Returns the given chains ordered based on the statsig config export function useOrderedChainIds(chainIds: UniverseChainId[]): UniverseChainId[] { diff --git a/packages/uniswap/src/features/chains/types.ts b/packages/uniswap/src/features/chains/types.ts index b302bfeafdb..4e680d421e8 100644 --- a/packages/uniswap/src/features/chains/types.ts +++ b/packages/uniswap/src/features/chains/types.ts @@ -1,10 +1,10 @@ // biome-ignore lint/style/noRestrictedImports: legacy import will be migrated import { CurrencyAmount, Token, ChainId as UniswapSDKChainId } from '@uniswap/sdk-core' import type { GraphQLApi } from '@universe/api' +import { SwapConfigKey } from '@universe/gating' import type { ImageSourcePropType } from 'react-native' // biome-ignore lint/style/noRestrictedImports: legacy import will be migrated import { type UNIVERSE_CHAIN_INFO } from 'uniswap/src/features/chains/chainInfo' -import { SwapConfigKey } from 'uniswap/src/features/gating/configs' import { Platform } from 'uniswap/src/features/platforms/types/Platform' import { ElementName } from 'uniswap/src/features/telemetry/constants' import { NonEmptyArray } from 'utilities/src/primitives/array' diff --git a/packages/uniswap/src/features/dataApi/balances/balances.ts b/packages/uniswap/src/features/dataApi/balances/balances.ts index 7fe5058c6ed..fa121a145af 100644 --- a/packages/uniswap/src/features/dataApi/balances/balances.ts +++ b/packages/uniswap/src/features/dataApi/balances/balances.ts @@ -38,10 +38,12 @@ export type PortfolioCacheUpdater = (hidden: boolean, portfolioBalance?: Portfol export function usePortfolioBalances({ evmAddress, svmAddress, + chainIds, ...queryOptions }: { evmAddress?: Address svmAddress?: Address + chainIds?: UniverseChainId[] } & QueryHookOptions< GraphQLApi.PortfolioBalancesQuery, GraphQLApi.PortfolioBalancesQueryVariables @@ -49,6 +51,7 @@ export function usePortfolioBalances({ return usePortfolioData({ evmAddress: evmAddress || '', svmAddress: svmAddress || '', + chainIds, ...queryOptions, skip: !(evmAddress ?? svmAddress) || queryOptions.skip, }) @@ -270,6 +273,8 @@ export function useTokenBalancesGroupedByVisibility({ }, [balancesById, currencyIdToTokenVisibility, isTestnetModeEnabled]) } +type SortedPortfolioBalancesResult = GqlResult & { networkStatus: NetworkStatus } + /** * Returns portfolio balances for a given address sorted by USD value. * @@ -284,12 +289,14 @@ export function useSortedPortfolioBalances({ svmAddress, pollInterval, onCompleted, + chainIds, }: { evmAddress?: Address svmAddress?: Address pollInterval?: PollingInterval onCompleted?: () => void -}): GqlResult & { networkStatus: NetworkStatus } { + chainIds?: UniverseChainId[] +}): SortedPortfolioBalancesResult { const { isTestnetModeEnabled } = useEnabledChains() // Fetch all balances including small balances and spam tokens because we want to return those in separate arrays @@ -304,19 +311,23 @@ export function useSortedPortfolioBalances({ pollInterval, onCompleted, fetchPolicy: 'cache-and-network', + chainIds, }) const { shownTokens, hiddenTokens } = useTokenBalancesGroupedByVisibility({ balancesById }) - return { - data: { - balances: sortPortfolioBalances({ balances: shownTokens || [], isTestnetModeEnabled }), - hiddenBalances: sortPortfolioBalances({ balances: hiddenTokens || [], isTestnetModeEnabled }), - }, - loading, - networkStatus, - refetch, - } + return useMemo( + () => ({ + data: { + balances: sortPortfolioBalances({ balances: shownTokens || [], isTestnetModeEnabled }), + hiddenBalances: sortPortfolioBalances({ balances: hiddenTokens || [], isTestnetModeEnabled }), + }, + loading, + networkStatus, + refetch, + }), + [shownTokens, hiddenTokens, isTestnetModeEnabled, loading, networkStatus, refetch], + ) } /** diff --git a/packages/uniswap/src/features/dataApi/balances/balancesRest.ts b/packages/uniswap/src/features/dataApi/balances/balancesRest.ts index 64f1f59fb9e..b288659bb99 100644 --- a/packages/uniswap/src/features/dataApi/balances/balancesRest.ts +++ b/packages/uniswap/src/features/dataApi/balances/balancesRest.ts @@ -48,7 +48,8 @@ export function usePortfolioData({ pollInterval?: PollingInterval fetchPolicy?: WatchQueryFetchPolicy } & GetPortfolioInput['input']): PortfolioDataResult { - const { chains: chainIds } = useEnabledChains() + const { chains: defaultChainIds } = useEnabledChains() + const chainIds = queryOptions.chainIds || defaultChainIds // TODO(SWAP-388): GetPortfolio REST endpoint does not yet support modifier array; it will take 1 evm/svm address, but will apply the modifications across the board const modifier = useRestPortfolioValueModifier(evmAddress ?? svmAddress) diff --git a/packages/uniswap/src/features/dataApi/tokenProjects/tokenProjects.test.tsx b/packages/uniswap/src/features/dataApi/tokenProjects/tokenProjects.test.tsx index 7bb8281ae25..d588ebf377b 100644 --- a/packages/uniswap/src/features/dataApi/tokenProjects/tokenProjects.test.tsx +++ b/packages/uniswap/src/features/dataApi/tokenProjects/tokenProjects.test.tsx @@ -28,9 +28,17 @@ describe(useTokenProjects, () => { resolvers, }) - await waitFor(async () => { - const data = result.current.data - expect(data).toEqual(tokenProjectToCurrencyInfos(await resolved.tokenProjects)) + const expected = tokenProjectToCurrencyInfos(await resolved.tokenProjects) + // GraphQL converts undefined to null, so we need to do the same for comparison + const expectedWithNull = expected.map((item) => ({ + ...item, + isBridged: item.isBridged ?? null, + bridgedWithdrawalInfo: item.bridgedWithdrawalInfo ?? null, + })) + + await waitFor(() => { + expect(result.current.loading).toEqual(false) + expect(result.current.data).toEqual(expectedWithNull) }) }) }) diff --git a/packages/uniswap/src/features/dataApi/tokenProjects/utils/tokenProjectToCurrencyInfos.test.ts b/packages/uniswap/src/features/dataApi/tokenProjects/utils/tokenProjectToCurrencyInfos.test.ts index 996eaa13b22..1ac2dc67a12 100644 --- a/packages/uniswap/src/features/dataApi/tokenProjects/utils/tokenProjectToCurrencyInfos.test.ts +++ b/packages/uniswap/src/features/dataApi/tokenProjects/utils/tokenProjectToCurrencyInfos.test.ts @@ -20,6 +20,8 @@ describe(tokenProjectToCurrencyInfos, () => { symbol: token.symbol, name: token.name ?? project.name, }), + isBridged: token.isBridged, + bridgedWithdrawalInfo: token.bridgedWithdrawalInfo, }) as CurrencyInfo it('converts tokenProject to CurrencyInfo', () => { diff --git a/packages/uniswap/src/features/dataApi/tokenProjects/utils/tokenProjectToCurrencyInfos.ts b/packages/uniswap/src/features/dataApi/tokenProjects/utils/tokenProjectToCurrencyInfos.ts index c84763a73e0..01c78c2e2e4 100644 --- a/packages/uniswap/src/features/dataApi/tokenProjects/utils/tokenProjectToCurrencyInfos.ts +++ b/packages/uniswap/src/features/dataApi/tokenProjects/utils/tokenProjectToCurrencyInfos.ts @@ -14,7 +14,8 @@ export function tokenProjectToCurrencyInfos( ?.flatMap((project) => project?.tokens.map((token) => { const { logoUrl, safetyLevel } = project - const { name, chain, address, decimals, symbol, feeData, protectionInfo } = token + const { name, chain, address, decimals, symbol, feeData, protectionInfo, isBridged, bridgedWithdrawalInfo } = + token const chainId = fromGraphQLChain(chain) if (chainFilter && chainFilter !== chainId) { @@ -40,6 +41,8 @@ export function tokenProjectToCurrencyInfos( currencyId: currencyId(currency), logoUrl, safetyInfo: getCurrencySafetyInfo(safetyLevel, protectionInfo), + isBridged, + bridgedWithdrawalInfo, }) return currencyInfo diff --git a/packages/uniswap/src/features/dataApi/types.ts b/packages/uniswap/src/features/dataApi/types.ts index 00138794455..acb786fb064 100644 --- a/packages/uniswap/src/features/dataApi/types.ts +++ b/packages/uniswap/src/features/dataApi/types.ts @@ -2,6 +2,7 @@ import { NetworkStatus } from '@apollo/client' import { Contract } from '@uniswap/client-data-api/dist/data/v1/types_pb' import { Currency } from '@uniswap/sdk-core' import { GraphQLApi, SpamCode } from '@universe/api' +import { BridgedWithdrawalInfo } from '@universe/api/src/clients/graphql/__generated__/types-and-hooks' import { FoTPercent } from 'uniswap/src/features/tokens/TokenWarningModal' import { CurrencyId } from 'uniswap/src/types/currency' @@ -45,6 +46,10 @@ export type CurrencyInfo = { isSpam?: Maybe // Indicates if this currency is from another chain than user searched isFromOtherNetwork?: boolean + // Indicates if this token is a bridged asset + isBridged?: Maybe + // Information about how to withdraw a bridged asset to its native chain + bridgedWithdrawalInfo?: Maybe } // Portfolio balance as exposed to the app diff --git a/packages/uniswap/src/features/dataApi/utils/gqlTokenToCurrencyInfo.test.ts b/packages/uniswap/src/features/dataApi/utils/gqlTokenToCurrencyInfo.test.ts index 75b58eea063..1df17498d9f 100644 --- a/packages/uniswap/src/features/dataApi/utils/gqlTokenToCurrencyInfo.test.ts +++ b/packages/uniswap/src/features/dataApi/utils/gqlTokenToCurrencyInfo.test.ts @@ -23,6 +23,8 @@ describe(gqlTokenToCurrencyInfo, () => { currencyId: `${fromGraphQLChain(token.chain)}-${token.address}`, logoUrl: token.project.logoUrl, isSpam: token.project.isSpam, + isBridged: token.isBridged, + bridgedWithdrawalInfo: token.bridgedWithdrawalInfo, }) }) diff --git a/packages/uniswap/src/features/dataApi/utils/gqlTokenToCurrencyInfo.ts b/packages/uniswap/src/features/dataApi/utils/gqlTokenToCurrencyInfo.ts index ce4d1560a4c..150c1063372 100644 --- a/packages/uniswap/src/features/dataApi/utils/gqlTokenToCurrencyInfo.ts +++ b/packages/uniswap/src/features/dataApi/utils/gqlTokenToCurrencyInfo.ts @@ -11,7 +11,8 @@ export type GqlTokenToCurrencyInfoToken = Omit, + ) => FormattedFiatDelta +} { + const currency = useAppFiatCurrency() + const { formatNumberOrString } = useLocalizationContext() + + return useMemo( + () => ({ + formatChartFiatDelta: ( + options: Omit, + ): FormattedFiatDelta => { + return formatChartFiatDelta({ ...options, currency, formatNumberOrString }) + }, + }), + [currency, formatNumberOrString], + ) +} diff --git a/packages/uniswap/src/features/fiatCurrency/priceChart/formatters/shared/types.ts b/packages/uniswap/src/features/fiatCurrency/priceChart/formatters/shared/types.ts new file mode 100644 index 00000000000..b86b504b881 --- /dev/null +++ b/packages/uniswap/src/features/fiatCurrency/priceChart/formatters/shared/types.ts @@ -0,0 +1,37 @@ +import { FiatCurrency } from 'uniswap/src/features/fiatCurrency/constants' +import { FormatNumberOrStringInput } from 'uniswap/src/features/language/formatter' + +export type DecimalPlaceNumber = number | 'threshold' | 'zero' + +export interface TrimTrailingZerosParams { + formatted: string + decimals: number + roundedValue?: number // Optional - only used by stablecoin formatter +} + +export interface FiatDeltaFormatter { + getDecimalPlaces: (absValue: number) => DecimalPlaceNumber + trimTrailingZeros: (params: TrimTrailingZerosParams) => string + shouldShowBelowThreshold: (absValue: number) => boolean + format: (params: FormatParams) => string +} + +export interface FormatParams { + value: number + currency: FiatCurrency + formatNumberOrString: (input: FormatNumberOrStringInput) => string +} + +export interface FiatDeltaFormatOptions { + startingPrice: number + endingPrice: number + isStablecoin?: boolean + currency?: FiatCurrency + formatNumberOrString: (input: FormatNumberOrStringInput) => string +} + +export interface FormattedFiatDelta { + formatted: string + rawDelta: number + belowThreshold?: boolean +} diff --git a/packages/uniswap/src/features/fiatCurrency/priceChart/formatters/shared/utils.ts b/packages/uniswap/src/features/fiatCurrency/priceChart/formatters/shared/utils.ts new file mode 100644 index 00000000000..0bc790e11a5 --- /dev/null +++ b/packages/uniswap/src/features/fiatCurrency/priceChart/formatters/shared/utils.ts @@ -0,0 +1,93 @@ +import { FiatCurrency } from 'uniswap/src/features/fiatCurrency/constants' +import { getFiatCurrencyCode } from 'uniswap/src/features/fiatCurrency/hooks' +import { TrimTrailingZerosParams } from 'uniswap/src/features/fiatCurrency/priceChart/formatters/shared/types' +import { FormatNumberOrStringInput } from 'uniswap/src/features/language/formatter' +import { NumberType } from 'utilities/src/format/types' + +export const FIAT_DELTA_THRESHOLD = 0.000001 +const FORMATTED_NUMBER_PATTERN = /^([^0-9]*)([0-9,.\s]+)(.*)$/ + +export function roundToDecimals(value: number, decimals: number): number { + const factor = Math.pow(10, decimals) + return Math.round(value * factor) / factor +} + +export function formatZero( + currency: FiatCurrency, + formatNumberOrString: (input: FormatNumberOrStringInput) => string, +): string { + const currencyCode = getFiatCurrencyCode(currency) + return formatNumberOrString({ + value: 0, + type: NumberType.FiatStandard, + currencyCode, + }) +} + +export function formatThreshold( + currency: FiatCurrency, + formatNumberOrString: (input: FormatNumberOrStringInput) => string, +): string { + const currencyCode = getFiatCurrencyCode(currency) + + // Format just the threshold value to get proper currency symbol + const formatted = formatNumberOrString({ + value: FIAT_DELTA_THRESHOLD, + type: NumberType.FiatTokenDetails, + currencyCode, + }) + + return `<${formatted}` +} + +export function formatWithDecimals(params: { + value: number + decimals: number + currency: FiatCurrency + formatNumberOrString: (input: FormatNumberOrStringInput) => string + trimZeros: (params: TrimTrailingZerosParams) => string +}): string { + const { value, decimals, currency, formatNumberOrString, trimZeros } = params + const absValue = Math.abs(value) + const currencyCode = getFiatCurrencyCode(currency) + + // Round the value to the specified decimals + const roundedValue = roundToDecimals(absValue, decimals) + + // For very small values, we need to use a different number type that preserves precision + // NumberType.FiatStandard uses StandardCurrency which defaults to 2 decimals + // NumberType.FiatTokenDetails uses rules that preserve precision for small values + let formatted: string + + if (decimals > 2 && roundedValue < 1) { + // Use FiatTokenDetails which has SmallestNumCurrency for small values + // This preserves up to 20 decimals and respects the user's locale + formatted = formatNumberOrString({ + value: roundedValue, + type: NumberType.FiatTokenDetails, + currencyCode, + }) + } else { + // For larger values or values with 2 decimals, use the standard formatter + formatted = formatNumberOrString({ + value: roundedValue, + type: NumberType.FiatStandard, + currencyCode, + }) + } + + return trimZeros({ formatted, decimals, roundedValue }) +} + +export function parseFormattedNumber(formatted: string): { + prefix: string + numberPart: string + suffix: string +} { + const match = formatted.match(FORMATTED_NUMBER_PATTERN) + if (!match) { + return { prefix: '', numberPart: formatted, suffix: '' } + } + const [, prefix = '', numberPart = '', suffix = ''] = match + return { prefix, numberPart, suffix } +} diff --git a/packages/uniswap/src/features/fiatCurrency/priceChart/formatters/stablecoinFormatter.ts b/packages/uniswap/src/features/fiatCurrency/priceChart/formatters/stablecoinFormatter.ts new file mode 100644 index 00000000000..4ff1d52a6fd --- /dev/null +++ b/packages/uniswap/src/features/fiatCurrency/priceChart/formatters/stablecoinFormatter.ts @@ -0,0 +1,84 @@ +import type { + DecimalPlaceNumber, + FiatDeltaFormatter, + TrimTrailingZerosParams, +} from 'uniswap/src/features/fiatCurrency/priceChart/formatters/shared/types' +import { + formatWithDecimals, + formatZero, + parseFormattedNumber, + roundToDecimals, +} from 'uniswap/src/features/fiatCurrency/priceChart/formatters/shared/utils' + +function getDecimalPlaces(absValue: number): DecimalPlaceNumber { + if (absValue === 0) { + return 'zero' + } + if (absValue >= 0.01) { + return 2 + } + if (absValue >= 0.001) { + return 3 + } + return 2 // Show $0.00 for < 0.001 +} + +function trimTrailingZeros(params: TrimTrailingZerosParams): string { + const { formatted, decimals, roundedValue } = params + + // Special handling for stablecoins that round to zero + // don't trim 3 decimal places + if (roundedValue === 0 || decimals === 3) { + return formatted + } + + const { prefix, numberPart, suffix } = parseFormattedNumber(formatted) + + // Only trim 00 for 2 decimal places + if (decimals === 2 && numberPart.match(/[.,]00$/)) { + return prefix + numberPart.replace(/[.,]00$/, '') + suffix + } + + return formatted +} + +export function createStablecoinFormatter(): FiatDeltaFormatter { + return { + getDecimalPlaces, + trimTrailingZeros, + + shouldShowBelowThreshold: () => false, // Never show threshold for stablecoins + + format: (params): string => { + const { value, currency, formatNumberOrString } = params + const absValue = Math.abs(value) + let decimals = getDecimalPlaces(absValue) + + if (decimals === 'zero') { + return formatZero(currency, formatNumberOrString) + } + + // Stablecoins treat values < 0.001 as zero (return $0.00) + if (absValue < 0.001 && absValue > 0) { + return formatZero(currency, formatNumberOrString) + } + + // Check if rounding changes which decimal bucket we're in + // For example, 0.0099 rounds to 0.01 with 3 decimals, which should use 2 decimals + if (decimals === 3) { + const rounded = roundToDecimals(absValue, 3) + if (rounded >= 0.01) { + decimals = 2 + } + } + + return formatWithDecimals({ + value, + decimals: decimals as number, + currency, + formatNumberOrString, + trimZeros: trimTrailingZeros, + }) + }, + } +} diff --git a/packages/uniswap/src/features/fiatCurrency/priceChart/formatters/standardFormatter.ts b/packages/uniswap/src/features/fiatCurrency/priceChart/formatters/standardFormatter.ts new file mode 100644 index 00000000000..f9597121a4c --- /dev/null +++ b/packages/uniswap/src/features/fiatCurrency/priceChart/formatters/standardFormatter.ts @@ -0,0 +1,88 @@ +import type { + DecimalPlaceNumber, + FiatDeltaFormatter, + TrimTrailingZerosParams, +} from 'uniswap/src/features/fiatCurrency/priceChart/formatters/shared/types' +import { + FIAT_DELTA_THRESHOLD, + formatThreshold, + formatWithDecimals, + formatZero, + parseFormattedNumber, +} from 'uniswap/src/features/fiatCurrency/priceChart/formatters/shared/utils' + +const DECIMAL_THRESHOLDS = [ + { min: 1, decimals: 2 }, + { min: 0.1, decimals: 2 }, + { min: 0.01, decimals: 3 }, + { min: 0.001, decimals: 4 }, + { min: 0.0001, decimals: 5 }, + { min: 0.00001, decimals: 6 }, +] as const + +function getDecimalPlaces(absValue: number): DecimalPlaceNumber { + if (absValue === 0) { + return 'zero' + } + + // Use a small epsilon for floating point comparison + const EPSILON = 1e-10 + + for (const { min, decimals } of DECIMAL_THRESHOLDS) { + // Use epsilon comparison to handle floating point errors + if (absValue >= min - EPSILON) { + return decimals + } + } + + return 'threshold' +} + +function trimTrailingZeros(params: TrimTrailingZerosParams): string { + const { formatted, decimals } = params + const { prefix, numberPart, suffix } = parseFormattedNumber(formatted) + let trimmed = numberPart + + if (decimals === 2) { + // For 2 decimal places, keep both decimals (don't trim trailing zeros) + // This ensures values like $0.10 stay as $0.10, not $0.1 + trimmed = numberPart + } else if (decimals > 2) { + // For decimals > 2, trim all trailing zeros (including the decimal point if no significant digits remain) + trimmed = numberPart.replace(/(\.\d*?)0+$/, '$1') + // If we end with just a decimal point, remove it + trimmed = trimmed.replace(/\.$/, '') + } + + return prefix + trimmed + suffix +} + +export function createStandardFormatter(): FiatDeltaFormatter { + return { + getDecimalPlaces, + trimTrailingZeros, + + shouldShowBelowThreshold: (absValue: number) => absValue > 0 && absValue < FIAT_DELTA_THRESHOLD, + + format: (params): string => { + const { value, currency, formatNumberOrString } = params + const absValue = Math.abs(value) + const decimals = getDecimalPlaces(absValue) + + switch (decimals) { + case 'zero': + return formatZero(currency, formatNumberOrString) + case 'threshold': + return formatThreshold(currency, formatNumberOrString) + default: + return formatWithDecimals({ + value, + decimals, + currency, + formatNumberOrString, + trimZeros: trimTrailingZeros, + }) + } + }, + } +} diff --git a/packages/uniswap/src/features/fiatCurrency/priceChart/priceChartConversion.test.ts b/packages/uniswap/src/features/fiatCurrency/priceChart/priceChartConversion.test.ts new file mode 100644 index 00000000000..fbf15548f29 --- /dev/null +++ b/packages/uniswap/src/features/fiatCurrency/priceChart/priceChartConversion.test.ts @@ -0,0 +1,1204 @@ +/* eslint-disable max-lines */ +import { FiatCurrency } from 'uniswap/src/features/fiatCurrency/constants' +import { formatChartFiatDelta } from 'uniswap/src/features/fiatCurrency/priceChart/priceChartConversion' +import { FormatNumberOrStringInput } from 'uniswap/src/features/language/formatter' + +// Minimal test formatter that matches expected test output +const defaultFormatter = (input: FormatNumberOrStringInput): string => { + const { value, currencyCode, type } = input + if (value === null || value === undefined) { + return '-' + } + + const num = typeof value === 'number' ? value : parseFloat(value) + const currencySymbols: Record = { + USD: '$', + GBP: '£', + EUR: 'EUR ', + JPY: '¥', + INR: '₹', + } + + const symbol = currencySymbols[currencyCode || 'USD'] || currencyCode + ' ' + + if (type === 'fiat-standard') { + let decimals = 2 + const str = num.toString() + const match = str.match(/\.(\d+)/) + if (match && match[1]) { + decimals = Math.max(2, match[1].length) + } + + const formatted = num.toLocaleString('en-US', { + minimumFractionDigits: decimals, + maximumFractionDigits: decimals, + }) + if (num >= 1 && num === Math.floor(num)) { + return `${symbol}${formatted.replace(/\.00$/, '')}` + } + return `${symbol}${formatted}` + } + + const getDecimals = (n: number): number => { + const abs = Math.abs(n) + if (abs === 0 || abs >= 0.1) { + return 2 + } + if (abs >= 0.01) { + return Math.max(2, n.toString().replace(/.*\./, '').replace(/0+$/, '').length) + } + if (abs >= 0.001) { + return Math.max(3, n.toString().replace(/.*\./, '').replace(/0+$/, '').length) + } + if (abs >= 0.0001) { + return Math.max(4, n.toString().replace(/.*\./, '').replace(/0+$/, '').length) + } + if (abs >= 0.00001) { + return Math.max(5, n.toString().replace(/.*\./, '').replace(/0+$/, '').length) + } + return 6 + } + + const decimals = getDecimals(num) + let formatted = num.toLocaleString('en-US', { + minimumFractionDigits: decimals, + maximumFractionDigits: decimals, + }) + + // Trim trailing zeros for whole numbers (e.g., $1.00 -> $1) + if (decimals === 2 && num >= 1 && num === Math.floor(num)) { + formatted = formatted.replace(/\.00$/, '') + } + + return `${symbol}${formatted}` +} + +describe('formatChartFiatDelta', () => { + describe('normal crypto formatting', () => { + describe('values >= $1', () => { + it('formats positive values with 2 decimals', () => { + expect( + formatChartFiatDelta({ startingPrice: 100, endingPrice: 101.25, formatNumberOrString: defaultFormatter }) + .formatted, + ).toBe('$1.25') + expect( + formatChartFiatDelta({ startingPrice: 100, endingPrice: 2530.1, formatNumberOrString: defaultFormatter }) + .formatted, + ).toBe('$2,430.10') + expect( + formatChartFiatDelta({ startingPrice: 100, endingPrice: 101, formatNumberOrString: defaultFormatter }) + .formatted, + ).toBe('$1') + expect( + formatChartFiatDelta({ startingPrice: 100, endingPrice: 1099.99, formatNumberOrString: defaultFormatter }) + .formatted, + ).toBe('$999.99') + }) + + it('formats negative values with 2 decimals', () => { + expect( + formatChartFiatDelta({ startingPrice: 100, endingPrice: 100 + -1.25, formatNumberOrString: defaultFormatter }) + .formatted, + ).toBe('$1.25') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + -2430.1, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$2,430.10') + expect( + formatChartFiatDelta({ startingPrice: 100, endingPrice: 100 + -1.0, formatNumberOrString: defaultFormatter }) + .formatted, + ).toBe('$1') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + -999.99, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$999.99') + }) + + it('uses thousand separators', () => { + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + 1234.56, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$1,234.56') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + 1234567.89, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$1,234,567.89') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + -1234567.89, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$1,234,567.89') + }) + + it('trims trailing zeros', () => { + expect( + formatChartFiatDelta({ startingPrice: 100, endingPrice: 100 + 1.0, formatNumberOrString: defaultFormatter }) + .formatted, + ).toBe('$1') + expect( + formatChartFiatDelta({ startingPrice: 100, endingPrice: 100 + 1.1, formatNumberOrString: defaultFormatter }) + .formatted, + ).toBe('$1.10') + expect( + formatChartFiatDelta({ startingPrice: 100, endingPrice: 100 + 1.2, formatNumberOrString: defaultFormatter }) + .formatted, + ).toBe('$1.20') + }) + }) + + describe('values >= $0.10 and < $1', () => { + it('formats positive values with 2 decimals', () => { + expect( + formatChartFiatDelta({ startingPrice: 100, endingPrice: 100 + 0.57, formatNumberOrString: defaultFormatter }) + .formatted, + ).toBe('$0.57') + expect( + formatChartFiatDelta({ startingPrice: 100, endingPrice: 100 + 0.14, formatNumberOrString: defaultFormatter }) + .formatted, + ).toBe('$0.14') + expect( + formatChartFiatDelta({ startingPrice: 100, endingPrice: 100 + 0.1, formatNumberOrString: defaultFormatter }) + .formatted, + ).toBe('$0.10') + expect( + formatChartFiatDelta({ startingPrice: 100, endingPrice: 100 + 0.99, formatNumberOrString: defaultFormatter }) + .formatted, + ).toBe('$0.99') + }) + + it('formats negative values with 2 decimals', () => { + expect( + formatChartFiatDelta({ startingPrice: 100, endingPrice: 100 + -0.57, formatNumberOrString: defaultFormatter }) + .formatted, + ).toBe('$0.57') + expect( + formatChartFiatDelta({ startingPrice: 100, endingPrice: 100 + -0.14, formatNumberOrString: defaultFormatter }) + .formatted, + ).toBe('$0.14') + expect( + formatChartFiatDelta({ startingPrice: 100, endingPrice: 100 + -0.1, formatNumberOrString: defaultFormatter }) + .formatted, + ).toBe('$0.10') + expect( + formatChartFiatDelta({ startingPrice: 100, endingPrice: 100 + -0.99, formatNumberOrString: defaultFormatter }) + .formatted, + ).toBe('$0.99') + }) + + it('trims trailing zeros', () => { + expect( + formatChartFiatDelta({ startingPrice: 100, endingPrice: 100 + 0.1, formatNumberOrString: defaultFormatter }) + .formatted, + ).toBe('$0.10') + expect( + formatChartFiatDelta({ startingPrice: 100, endingPrice: 100 + 0.5, formatNumberOrString: defaultFormatter }) + .formatted, + ).toBe('$0.50') + }) + }) + + describe('values >= $0.01 and < $0.10', () => { + it('formats positive values with 3 decimals', () => { + expect( + formatChartFiatDelta({ startingPrice: 100, endingPrice: 100 + 0.053, formatNumberOrString: defaultFormatter }) + .formatted, + ).toBe('$0.053') + expect( + formatChartFiatDelta({ startingPrice: 100, endingPrice: 100 + 0.096, formatNumberOrString: defaultFormatter }) + .formatted, + ).toBe('$0.096') + expect( + formatChartFiatDelta({ startingPrice: 100, endingPrice: 100 + 0.01, formatNumberOrString: defaultFormatter }) + .formatted, + ).toBe('$0.01') + expect( + formatChartFiatDelta({ startingPrice: 100, endingPrice: 100 + 0.099, formatNumberOrString: defaultFormatter }) + .formatted, + ).toBe('$0.099') + }) + + it('formats negative values with 3 decimals', () => { + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + -0.053, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.053') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + -0.096, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.096') + expect( + formatChartFiatDelta({ startingPrice: 100, endingPrice: 100 + -0.01, formatNumberOrString: defaultFormatter }) + .formatted, + ).toBe('$0.01') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + -0.099, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.099') + }) + + it('trims trailing zeros but keeps at least 2 decimals', () => { + expect( + formatChartFiatDelta({ startingPrice: 100, endingPrice: 100 + 0.01, formatNumberOrString: defaultFormatter }) + .formatted, + ).toBe('$0.01') + expect( + formatChartFiatDelta({ startingPrice: 100, endingPrice: 100 + 0.05, formatNumberOrString: defaultFormatter }) + .formatted, + ).toBe('$0.05') + }) + }) + + describe('values >= $0.001 and < $0.01', () => { + it('formats positive values with 4 decimals', () => { + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + 0.0075, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.0075') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + 0.0031, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.0031') + expect( + formatChartFiatDelta({ startingPrice: 100, endingPrice: 100 + 0.001, formatNumberOrString: defaultFormatter }) + .formatted, + ).toBe('$0.001') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + 0.0099, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.0099') + }) + + it('formats negative values with 4 decimals', () => { + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + -0.0075, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.0075') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + -0.0031, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.0031') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + -0.001, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.001') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + -0.0099, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.0099') + }) + + it('trims trailing zeros but keeps minimum decimals', () => { + expect( + formatChartFiatDelta({ startingPrice: 100, endingPrice: 100 + 0.001, formatNumberOrString: defaultFormatter }) + .formatted, + ).toBe('$0.001') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + 0.0012, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.0012') + }) + }) + + describe('values >= $0.0001 and < $0.001', () => { + it('formats positive values with 5 decimals', () => { + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + 0.00083, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.00083') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + 0.00022, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.00022') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + 0.0001, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.0001') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + 0.00099, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.00099') + }) + + it('formats negative values with 5 decimals', () => { + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + -0.00083, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.00083') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + -0.00022, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.00022') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + -0.0001, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.0001') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + -0.00099, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.00099') + }) + + it('trims trailing zeros but keeps minimum decimals', () => { + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + 0.0001, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.0001') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + 0.00012, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.00012') + }) + }) + + describe('values >= $0.00001 and < $0.0001', () => { + it('formats positive values with 6 decimals', () => { + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + 0.000019, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.000019') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + 0.000094, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.000094') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + 0.00001, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.00001') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + 0.000099, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.000099') + }) + + it('formats negative values with 6 decimals', () => { + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + -0.000019, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.000019') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + -0.000094, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.000094') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + -0.00001, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.00001') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + -0.000099, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.000099') + }) + + it('trims trailing zeros but keeps minimum decimals', () => { + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + 0.00001, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.00001') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + 0.000012, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.000012') + }) + }) + + describe('values < $0.000001', () => { + it('formats as threshold with exact value in tooltip', () => { + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + 0.0000001, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('<$0.000001') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + 0.0000009, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('<$0.000001') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + -0.0000001, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('<$0.000001') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + -0.0000009, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('<$0.000001') + }) + }) + + describe('zero value', () => { + it('formats as $0.00', () => { + expect( + formatChartFiatDelta({ startingPrice: 100, endingPrice: 100 + 0, formatNumberOrString: defaultFormatter }) + .formatted, + ).toBe('$0.00') + }) + }) + }) + + describe('stablecoin formatting', () => { + describe('values >= $1', () => { + it('formats positive values with 2 decimals', () => { + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + 1.25, + isStablecoin: true, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$1.25') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + 2430.1, + isStablecoin: true, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$2,430.10') + }) + + it('formats negative values with 2 decimals', () => { + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + -1.25, + isStablecoin: true, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$1.25') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + -2430.1, + isStablecoin: true, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$2,430.10') + }) + }) + + describe('values >= $0.10 and < $1', () => { + it('formats positive values with 2 decimals', () => { + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + 0.42, + isStablecoin: true, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.42') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + 0.1, + isStablecoin: true, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.10') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + 0.99, + isStablecoin: true, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.99') + }) + + it('formats negative values with 2 decimals', () => { + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + -0.42, + isStablecoin: true, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.42') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + -0.1, + isStablecoin: true, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.10') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + -0.99, + isStablecoin: true, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.99') + }) + }) + + describe('values >= $0.01 and < $0.10', () => { + it('formats positive values with 2 decimals', () => { + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + 0.07, + isStablecoin: true, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.07') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + 0.01, + isStablecoin: true, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.01') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + 0.099, + isStablecoin: true, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.10') + }) + + it('formats negative values with 2 decimals', () => { + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + -0.07, + isStablecoin: true, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.07') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + -0.01, + isStablecoin: true, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.01') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + -0.099, + isStablecoin: true, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.10') + }) + }) + + describe('values >= $0.001 and < $0.01', () => { + it('formats positive values with 3 decimals', () => { + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + 0.003, + isStablecoin: true, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.003') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + 0.001, + isStablecoin: true, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.001') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + 0.0099, + isStablecoin: true, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.01') + }) + + it('formats negative values with 3 decimals', () => { + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + -0.003, + isStablecoin: true, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.003') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + -0.001, + isStablecoin: true, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.001') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + -0.0099, + isStablecoin: true, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.01') + }) + }) + + describe('values < $0.001', () => { + it('formats as $0.00', () => { + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + 0.0001, + isStablecoin: true, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.00') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + 0.0009, + isStablecoin: true, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.00') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + -0.0001, + isStablecoin: true, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.00') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + -0.0009, + isStablecoin: true, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.00') + }) + }) + + describe('zero value', () => { + it('formats as $0.00', () => { + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + 0, + isStablecoin: true, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.00') + }) + }) + }) + + describe('formatFiatDelta with isStablecoin flag', () => { + it('uses stablecoin formatting when isStablecoin is true', () => { + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + 0.003, + isStablecoin: true, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.003') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + 0.0001, + isStablecoin: true, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.00') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + 1.25, + isStablecoin: true, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$1.25') + }) + + it('uses normal formatting when isStablecoin is false', () => { + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + 0.003, + isStablecoin: false, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.003') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + 0.0001, + isStablecoin: false, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.0001') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + 1.25, + isStablecoin: false, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$1.25') + }) + }) + + describe('currency support', () => { + it('formats EUR currency with proper symbol', () => { + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + 100, + currency: FiatCurrency.Euro, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('EUR 100') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + -50.5, + currency: FiatCurrency.Euro, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('EUR 50.50') + }) + + it('formats GBP currency with proper symbol', () => { + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + 100, + currency: FiatCurrency.BritishPound, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('£100') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + -50.5, + currency: FiatCurrency.BritishPound, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('£50.50') + }) + + it('formats JPY currency with proper symbol', () => { + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + 100, + currency: FiatCurrency.JapaneseYen, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('¥100') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + -50.5, + currency: FiatCurrency.JapaneseYen, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('¥50.50') + }) + }) + + describe('edge cases', () => { + it('handles trimming edge cases that previously failed', () => { + // Test case 1: Value that trims to whole number (previously would fail regex) + // 1.000 with 4 decimals should trim to "1", not break + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 101.0, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$1') + + // Test case 2: Value with all trailing zeros after rounding + // 0.0010 with 4 decimals should trim to "0.001" + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100.001, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.001') + + // Test case 3: Value in the 3-decimal range that has trailing zeros + // 0.0500 should trim to "0.05" (in the 3-decimal range >= 0.01 and < 0.10) + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100.05, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.05') + + // Test case 4: Very small value that needs all its decimals preserved + // 0.00012 should keep all significant digits + expect( + formatChartFiatDelta({ + startingPrice: 1.0, + endingPrice: 1.00012, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.00012') + + // Test case 5: Value that would have broken the old regex when decimal point is removed + // 0.001000 with 4 decimals trims to "0.001", old logic would try to match non-existent decimal + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100.001, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('$0.001') + }) + + it('handles rounding correctly', () => { + expect( + formatChartFiatDelta({ startingPrice: 100, endingPrice: 100 + 0.0999, formatNumberOrString: defaultFormatter }) + .formatted, + ).toBe('$0.1') + expect( + formatChartFiatDelta({ startingPrice: 100, endingPrice: 100 + 0.00999, formatNumberOrString: defaultFormatter }) + .formatted, + ).toBe('$0.01') + expect( + formatChartFiatDelta({ startingPrice: 100, endingPrice: 100 + 0.0994, formatNumberOrString: defaultFormatter }) + .formatted, + ).toBe('$0.099') + }) + + it('handles very small negative values correctly', () => { + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + -0.0000001, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('<$0.000001') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + -0.000001, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('<$0.000001') + }) + + it('handles values exactly at thresholds', () => { + expect( + formatChartFiatDelta({ startingPrice: 100, endingPrice: 100 + 1.0, formatNumberOrString: defaultFormatter }) + .formatted, + ).toBe('$1') + expect( + formatChartFiatDelta({ startingPrice: 100, endingPrice: 100 + 0.1, formatNumberOrString: defaultFormatter }) + .formatted, + ).toBe('$0.10') + expect( + formatChartFiatDelta({ startingPrice: 100, endingPrice: 100 + 0.01, formatNumberOrString: defaultFormatter }) + .formatted, + ).toBe('$0.01') + expect( + formatChartFiatDelta({ startingPrice: 100, endingPrice: 100 + 0.001, formatNumberOrString: defaultFormatter }) + .formatted, + ).toBe('$0.001') + expect( + formatChartFiatDelta({ startingPrice: 100, endingPrice: 100 + 0.0001, formatNumberOrString: defaultFormatter }) + .formatted, + ).toBe('$0.0001') + expect( + formatChartFiatDelta({ startingPrice: 100, endingPrice: 100 + 0.00001, formatNumberOrString: defaultFormatter }) + .formatted, + ).toBe('$0.00001') + expect( + formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100 + 0.000001, + formatNumberOrString: defaultFormatter, + }).formatted, + ).toBe('<$0.000001') + }) + }) + describe('delta calculation', () => { + it('calculates and formats positive delta for normal crypto', () => { + const result = formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 103.53, + isStablecoin: false, + formatNumberOrString: defaultFormatter, + }) + expect(result.formatted).toBe('$3.53') + expect(result.rawDelta).toBeCloseTo(3.53, 5) + expect(result.belowThreshold).toBeUndefined() + }) + + it('correctly formats DEGEN-like small delta values', () => { + // Test the specific DEGEN case: $0.00370 - $0.00338 = $0.00032 + const result = formatChartFiatDelta({ + startingPrice: 0.00338, + endingPrice: 0.0037, + isStablecoin: false, + formatNumberOrString: defaultFormatter, + }) + expect(result.formatted).toBe('$0.00032') + expect(result.rawDelta).toBeCloseTo(0.00032, 10) + expect(result.belowThreshold).toBeUndefined() + }) + + it('calculates and formats positive delta for stablecoin', () => { + const result = formatChartFiatDelta({ + startingPrice: 1.0, + endingPrice: 1.003, + isStablecoin: true, + formatNumberOrString: defaultFormatter, + }) + expect(result.formatted).toBe('$0.003') + expect(result.rawDelta).toBeCloseTo(0.003, 5) + expect(result.belowThreshold).toBeUndefined() + }) + + it('calculates and formats negative delta for stablecoin', () => { + const result = formatChartFiatDelta({ + startingPrice: 1.0, + endingPrice: 0.997, + isStablecoin: true, + formatNumberOrString: defaultFormatter, + }) + expect(result.formatted).toBe('$0.003') + expect(result.rawDelta).toBeCloseTo(-0.003, 5) + expect(result.belowThreshold).toBeUndefined() + }) + + it('handles very small deltas for stablecoin', () => { + const result = formatChartFiatDelta({ + startingPrice: 1.0, + endingPrice: 1.0001, + isStablecoin: true, + formatNumberOrString: defaultFormatter, + }) + expect(result.formatted).toBe('$0.00') + expect(result.rawDelta).toBeCloseTo(0.0001, 5) + expect(result.belowThreshold).toBeUndefined() + }) + + it('handles zero delta', () => { + const result = formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 100, + isStablecoin: false, + formatNumberOrString: defaultFormatter, + }) + expect(result.formatted).toBe('$0.00') + expect(result.rawDelta).toBe(0) + expect(result.belowThreshold).toBeUndefined() + }) + + it('uses custom currency', () => { + const result = formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 150, + currency: FiatCurrency.Euro, + formatNumberOrString: defaultFormatter, + }) + expect(result.formatted).toBe('EUR 50') + expect(result.rawDelta).toBe(50) + expect(result.belowThreshold).toBeUndefined() + }) + + it('handles below threshold values for normal crypto', () => { + const result = formatChartFiatDelta({ + startingPrice: 1.0, + endingPrice: 1.0000005, + isStablecoin: false, + formatNumberOrString: defaultFormatter, + }) + expect(result.formatted).toBe('<$0.000001') + expect(result.rawDelta).toBeCloseTo(0.0000005, 10) + expect(result.belowThreshold).toBe(true) + }) + + it('handles multiple currencies', () => { + const gbpResult = formatChartFiatDelta({ + startingPrice: 100, + endingPrice: 175.25, + currency: FiatCurrency.BritishPound, + formatNumberOrString: defaultFormatter, + }) + expect(gbpResult.formatted).toBe('£75.25') + expect(gbpResult.rawDelta).toBe(75.25) + + const jpyResult = formatChartFiatDelta({ + startingPrice: 10000, + endingPrice: 12500, + currency: FiatCurrency.JapaneseYen, + formatNumberOrString: defaultFormatter, + }) + expect(jpyResult.formatted).toBe('¥2,500') + expect(jpyResult.rawDelta).toBe(2500) + + const inrResult = formatChartFiatDelta({ + startingPrice: 5000, + endingPrice: 7500.5, + currency: FiatCurrency.IndianRupee, + formatNumberOrString: defaultFormatter, + }) + expect(inrResult.formatted).toBe('₹2,500.50') + expect(inrResult.rawDelta).toBe(2500.5) + }) + + it('correctly sets belowThreshold flag', () => { + // Should set belowThreshold for non-stablecoins + const cryptoResult = formatChartFiatDelta({ + startingPrice: 1.0, + endingPrice: 1.0000003, + isStablecoin: false, + formatNumberOrString: defaultFormatter, + }) + expect(cryptoResult.belowThreshold).toBe(true) + + // Should NOT set belowThreshold for stablecoins + const stableResult = formatChartFiatDelta({ + startingPrice: 1.0, + endingPrice: 1.0000003, + isStablecoin: true, + formatNumberOrString: defaultFormatter, + }) + expect(stableResult.belowThreshold).toBeUndefined() + + // Should NOT set belowThreshold when value is above threshold + const aboveResult = formatChartFiatDelta({ + startingPrice: 1.0, + endingPrice: 1.001, + isStablecoin: false, + formatNumberOrString: defaultFormatter, + }) + expect(aboveResult.belowThreshold).toBeUndefined() + }) + }) +}) diff --git a/packages/uniswap/src/features/fiatCurrency/priceChart/priceChartConversion.ts b/packages/uniswap/src/features/fiatCurrency/priceChart/priceChartConversion.ts new file mode 100644 index 00000000000..91e80a8d9ee --- /dev/null +++ b/packages/uniswap/src/features/fiatCurrency/priceChart/priceChartConversion.ts @@ -0,0 +1,42 @@ +import { FiatCurrency } from 'uniswap/src/features/fiatCurrency/constants' +import type { + FiatDeltaFormatOptions, + FormattedFiatDelta, +} from 'uniswap/src/features/fiatCurrency/priceChart/formatters/shared/types' +import { createStablecoinFormatter } from 'uniswap/src/features/fiatCurrency/priceChart/formatters/stablecoinFormatter' +import { createStandardFormatter } from 'uniswap/src/features/fiatCurrency/priceChart/formatters/standardFormatter' + +/** + * Utility for formatting fiat currency delta values in price charts. + * + * This module provides specialized formatting for price change amounts with: + * - Dynamic decimal precision based on value magnitude (2-6 decimal places) + * - Threshold formatting for very small values (<$0.000001) + * - Intelligent trailing zero trimming while preserving minimum decimals + * - Support for multiple fiat currencies with proper symbol extraction + * - Special handling for stablecoins (simplified precision rules) + */ +export function formatChartFiatDelta({ + startingPrice, + endingPrice, + isStablecoin = false, + currency = FiatCurrency.UnitedStatesDollar, + formatNumberOrString, +}: FiatDeltaFormatOptions): FormattedFiatDelta { + const formatter = isStablecoin ? createStablecoinFormatter() : createStandardFormatter() + const rawDelta = endingPrice - startingPrice + + const formatted = formatter.format({ + value: rawDelta, + currency, + formatNumberOrString, + }) + + const belowThreshold = formatter.shouldShowBelowThreshold(Math.abs(rawDelta)) + + return { + formatted, + rawDelta, + ...(belowThreshold && { belowThreshold: true }), + } +} diff --git a/packages/uniswap/src/features/forceUpgrade/hooks/useForceUpgradeStatus.ts b/packages/uniswap/src/features/forceUpgrade/hooks/useForceUpgradeStatus.ts index 246af09288d..b573fee07df 100644 --- a/packages/uniswap/src/features/forceUpgrade/hooks/useForceUpgradeStatus.ts +++ b/packages/uniswap/src/features/forceUpgrade/hooks/useForceUpgradeStatus.ts @@ -1,5 +1,4 @@ -import { DynamicConfigs, ForceUpgradeConfigKey, ForceUpgradeStatus } from 'uniswap/src/features/gating/configs' -import { useDynamicConfigValue } from 'uniswap/src/features/gating/hooks' +import { DynamicConfigs, ForceUpgradeConfigKey, ForceUpgradeStatus, useDynamicConfigValue } from '@universe/gating' export function useForceUpgradeStatus(): ForceUpgradeStatus { return useDynamicConfigValue({ diff --git a/packages/uniswap/src/features/forceUpgrade/hooks/useForceUpgradeTranslations.ts b/packages/uniswap/src/features/forceUpgrade/hooks/useForceUpgradeTranslations.ts index 5eb16d3693e..63357d0ef22 100644 --- a/packages/uniswap/src/features/forceUpgrade/hooks/useForceUpgradeTranslations.ts +++ b/packages/uniswap/src/features/forceUpgrade/hooks/useForceUpgradeTranslations.ts @@ -1,5 +1,9 @@ -import { DynamicConfigs, ForceUpgradeConfigKey, ForceUpgradeTranslations } from 'uniswap/src/features/gating/configs' -import { useDynamicConfigValue } from 'uniswap/src/features/gating/hooks' +import { + DynamicConfigs, + ForceUpgradeConfigKey, + ForceUpgradeTranslations, + useDynamicConfigValue, +} from '@universe/gating' export function useForceUpgradeTranslations(): ForceUpgradeTranslations { return useDynamicConfigValue({ diff --git a/packages/uniswap/src/features/gas/hooks.ts b/packages/uniswap/src/features/gas/hooks.ts index 96ae3a1cd70..cab1383e613 100644 --- a/packages/uniswap/src/features/gas/hooks.ts +++ b/packages/uniswap/src/features/gas/hooks.ts @@ -1,5 +1,6 @@ import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import { GasStrategy } from '@universe/api' +import { GasStrategyType, useStatsigClientStatus } from '@universe/gating' import { BigNumber, providers } from 'ethers/lib/ethers' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' @@ -12,8 +13,6 @@ import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledCh import { UniverseChainId } from 'uniswap/src/features/chains/types' import { FormattedUniswapXGasFeeInfo, GasFeeResult } from 'uniswap/src/features/gas/types' import { getActiveGasStrategy, hasSufficientFundsIncludingGas } from 'uniswap/src/features/gas/utils' -import { GasStrategyType } from 'uniswap/src/features/gating/configs' -import { useStatsigClientStatus } from 'uniswap/src/features/gating/hooks' import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext' import { useOnChainNativeCurrencyBalance } from 'uniswap/src/features/portfolio/api' import { getCurrencyAmount, ValueType } from 'uniswap/src/features/tokens/getCurrencyAmount' diff --git a/packages/uniswap/src/features/gas/hooks/useMaxAmountSpend.test.ts b/packages/uniswap/src/features/gas/hooks/useMaxAmountSpend.test.ts index 5019f99318b..e1717ddd237 100644 --- a/packages/uniswap/src/features/gas/hooks/useMaxAmountSpend.test.ts +++ b/packages/uniswap/src/features/gas/hooks/useMaxAmountSpend.test.ts @@ -8,8 +8,9 @@ import { MAINNET_CURRENCY } from 'uniswap/src/test/fixtures/wallet/currencies' const mockUseDynamicConfigValue = jest.fn() -jest.mock('uniswap/src/features/gating/hooks', () => { +jest.mock('@universe/gating', () => { return { + ...jest.requireActual('@universe/gating'), useDynamicConfigValue: (params: { config: unknown; key: unknown; defaultValue: unknown }): unknown => mockUseDynamicConfigValue(params), } diff --git a/packages/uniswap/src/features/gas/hooks/useMaxAmountSpend.ts b/packages/uniswap/src/features/gas/hooks/useMaxAmountSpend.ts index 7d223059153..30a1e1ceb77 100644 --- a/packages/uniswap/src/features/gas/hooks/useMaxAmountSpend.ts +++ b/packages/uniswap/src/features/gas/hooks/useMaxAmountSpend.ts @@ -1,10 +1,9 @@ import { Currency, CurrencyAmount } from '@uniswap/sdk-core' +import { DynamicConfigs, SwapConfigKey, useDynamicConfigValue } from '@universe/gating' import JSBI from 'jsbi' import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' import { GENERIC_L2_GAS_CONFIG } from 'uniswap/src/features/chains/gasDefaults' import { UniverseChainId } from 'uniswap/src/features/chains/types' -import { DynamicConfigs, SwapConfigKey } from 'uniswap/src/features/gating/configs' -import { useDynamicConfigValue } from 'uniswap/src/features/gating/hooks' import { getCurrencyAmount, ValueType } from 'uniswap/src/features/tokens/getCurrencyAmount' import { TransactionType } from 'uniswap/src/features/transactions/types/transactionDetails' diff --git a/packages/uniswap/src/features/gas/utils.ts b/packages/uniswap/src/features/gas/utils.ts index f8862b530fa..9c1e2460928 100644 --- a/packages/uniswap/src/features/gas/utils.ts +++ b/packages/uniswap/src/features/gas/utils.ts @@ -1,14 +1,14 @@ import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import { GasEstimate, GasStrategy } from '@universe/api' -import JSBI from 'jsbi' -import { areEqualGasStrategies } from 'uniswap/src/features/gas/types' import { DynamicConfigs, GasStrategies, GasStrategyType, GasStrategyWithConditions, -} from 'uniswap/src/features/gating/configs' -import { getStatsigClient } from 'uniswap/src/features/gating/sdk/statsig' + getStatsigClient, +} from '@universe/gating' +import JSBI from 'jsbi' +import { areEqualGasStrategies } from 'uniswap/src/features/gas/types' import { getCurrencyAmount, ValueType } from 'uniswap/src/features/tokens/getCurrencyAmount' // The default "Urgent" strategy that was previously hardcoded in the gas service diff --git a/packages/uniswap/src/features/gating/StatsigProviderWrapper.tsx b/packages/uniswap/src/features/gating/StatsigProviderWrapper.tsx index 86c703873e0..f73bb72db2a 100644 --- a/packages/uniswap/src/features/gating/StatsigProviderWrapper.tsx +++ b/packages/uniswap/src/features/gating/StatsigProviderWrapper.tsx @@ -1,12 +1,6 @@ +import { StatsigOptions, StatsigProvider, StatsigUser, StorageProvider, useClientAsyncInit } from '@universe/gating' import { ReactNode, useEffect } from 'react' import { config } from 'uniswap/src/config' -import { - StatsigOptions, - StatsigProvider, - StatsigUser, - StorageProvider, - useClientAsyncInit, -} from 'uniswap/src/features/gating/sdk/statsig' import { statsigBaseConfig } from 'uniswap/src/features/gating/statsigBaseConfig' import { logger } from 'utilities/src/logger/logger' diff --git a/packages/uniswap/src/features/gating/statsigBaseConfig.ts b/packages/uniswap/src/features/gating/statsigBaseConfig.ts index 6ea6d023740..dbb1f13d574 100644 --- a/packages/uniswap/src/features/gating/statsigBaseConfig.ts +++ b/packages/uniswap/src/features/gating/statsigBaseConfig.ts @@ -1,6 +1,5 @@ +import { getOverrideAdapter, getStatsigEnvName, StatsigOptions } from '@universe/gating' import { uniswapUrls } from 'uniswap/src/constants/urls' -import { getStatsigEnvName } from 'uniswap/src/features/gating/getStatsigEnvName' -import { getOverrideAdapter, StatsigOptions } from 'uniswap/src/features/gating/sdk/statsig' export const statsigBaseConfig: StatsigOptions = { networkConfig: { api: uniswapUrls.statsigProxyUrl }, diff --git a/packages/uniswap/src/features/gating/typeGuards.ts b/packages/uniswap/src/features/gating/typeGuards.ts index 4f98aa75596..058c3fb0549 100644 --- a/packages/uniswap/src/features/gating/typeGuards.ts +++ b/packages/uniswap/src/features/gating/typeGuards.ts @@ -1,6 +1,6 @@ import { GraphQLApi } from '@universe/api' +import { UwULinkAllowlist } from '@universe/gating' import { UniverseChainId } from 'uniswap/src/features/chains/types' -import { UwULinkAllowlist } from 'uniswap/src/features/gating/configs' export const isUwULinkAllowlistType = (x: unknown): x is UwULinkAllowlist => { const hasFields = diff --git a/packages/uniswap/src/features/language/hooks.tsx b/packages/uniswap/src/features/language/hooks.tsx index 1c550e5213d..5e76bb6999f 100644 --- a/packages/uniswap/src/features/language/hooks.tsx +++ b/packages/uniswap/src/features/language/hooks.tsx @@ -1,9 +1,9 @@ +import { ForceUpgradeTranslations } from '@universe/gating' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' import { AppTFunction } from 'ui/src/i18n/types' import { useUrlContext } from 'uniswap/src/contexts/UrlContext' -import { ForceUpgradeTranslations } from 'uniswap/src/features/gating/configs' import { Language, Locale, diff --git a/packages/uniswap/src/features/nfts/hooks/useNftContextMenuItems.tsx b/packages/uniswap/src/features/nfts/hooks/useNftContextMenuItems.tsx index afebb23bf45..99c9236debd 100644 --- a/packages/uniswap/src/features/nfts/hooks/useNftContextMenuItems.tsx +++ b/packages/uniswap/src/features/nfts/hooks/useNftContextMenuItems.tsx @@ -1,4 +1,5 @@ import { TokenReportEventType } from '@universe/api' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' @@ -14,8 +15,6 @@ import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledCh import { useBlockExplorerLogo } from 'uniswap/src/features/chains/logos' import { type UniverseChainId } from 'uniswap/src/features/chains/types' import { getChainExplorerName } from 'uniswap/src/features/chains/utils' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { useNavigateToNftExplorerLink } from 'uniswap/src/features/nfts/hooks/useNavigateToNftExplorerLink' import { getIsNftHidden, getNFTAssetKey } from 'uniswap/src/features/nfts/utils' import { pushNotification } from 'uniswap/src/features/notifications/slice/slice' diff --git a/packages/uniswap/src/features/passkey/hooks/useEmbeddedWalletBaseUrl.ts b/packages/uniswap/src/features/passkey/hooks/useEmbeddedWalletBaseUrl.ts index 116f82ffc2b..36eeab329b6 100644 --- a/packages/uniswap/src/features/passkey/hooks/useEmbeddedWalletBaseUrl.ts +++ b/packages/uniswap/src/features/passkey/hooks/useEmbeddedWalletBaseUrl.ts @@ -1,6 +1,5 @@ +import { DynamicConfigs, EmbeddedWalletConfigKey, useDynamicConfigValue } from '@universe/gating' import { UNISWAP_WEB_URL } from 'uniswap/src/constants/urls' -import { DynamicConfigs, EmbeddedWalletConfigKey } from 'uniswap/src/features/gating/configs' -import { useDynamicConfigValue } from 'uniswap/src/features/gating/hooks' export function useEmbeddedWalletBaseUrl(): string { const baseUrl = useDynamicConfigValue({ diff --git a/packages/uniswap/src/features/portfolio/PortfolioBalance/PortfolioBalance.tsx b/packages/uniswap/src/features/portfolio/PortfolioBalance/PortfolioBalance.tsx index 48a9aeccb6b..d5327f5640a 100644 --- a/packages/uniswap/src/features/portfolio/PortfolioBalance/PortfolioBalance.tsx +++ b/packages/uniswap/src/features/portfolio/PortfolioBalance/PortfolioBalance.tsx @@ -17,9 +17,13 @@ import { isWebPlatform } from 'utilities/src/platform' interface PortfolioBalanceProps { owner: Address + endText?: JSX.Element | string } -export const PortfolioBalance = memo(function _PortfolioBalance({ owner }: PortfolioBalanceProps): JSX.Element { +export const PortfolioBalance = memo(function _PortfolioBalance({ + owner, + endText, +}: PortfolioBalanceProps): JSX.Element { const { data, loading, networkStatus, refetch } = usePortfolioTotalValue({ evmAddress: owner, // TransactionHistoryUpdater will refetch this query on new transaction. @@ -47,7 +51,7 @@ export const PortfolioBalance = memo(function _PortfolioBalance({ owner }: Portf const shouldFadePortfolioDecimals = (currency === FiatCurrency.UnitedStatesDollar || currency === FiatCurrency.Euro) && currencyComponents.symbolAtFront - const EndElement = useMemo(() => { + const RefreshButton = useMemo(() => { if (isWebPlatform) { return } @@ -65,19 +69,22 @@ export const PortfolioBalance = memo(function _PortfolioBalance({ owner }: Portf value={totalBalance} warmLoading={isWarmLoading} isRightToLeft={isRightToLeft} - EndElement={EndElement} + EndElement={RefreshButton} /> - - - + + + + + {endText} + ) }) diff --git a/packages/uniswap/src/features/portfolio/api.ts b/packages/uniswap/src/features/portfolio/api.ts index 8bbe8b0e570..910338e10cc 100644 --- a/packages/uniswap/src/features/portfolio/api.ts +++ b/packages/uniswap/src/features/portfolio/api.ts @@ -2,6 +2,7 @@ import { PublicKey } from '@solana/web3.js' import { skipToken, useQuery } from '@tanstack/react-query' import { Currency, CurrencyAmount, NativeCurrency as NativeCurrencyClass } from '@uniswap/sdk-core' import { SharedQueryClient } from '@universe/api' +import { DynamicConfigs, getDynamicConfigValue, SyncTransactionSubmissionChainIdsConfigKey } from '@universe/gating' import { Contract } from 'ethers/lib/ethers' import { useMemo } from 'react' import ERC20_ABI from 'uniswap/src/abis/erc20.json' @@ -14,8 +15,6 @@ import { import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { getPollingIntervalByBlocktime } from 'uniswap/src/features/chains/utils' -import { DynamicConfigs, SyncTransactionSubmissionChainIdsConfigKey } from 'uniswap/src/features/gating/configs' -import { getDynamicConfigValue } from 'uniswap/src/features/gating/hooks' import { Platform } from 'uniswap/src/features/platforms/types/Platform' import { chainIdToPlatform } from 'uniswap/src/features/platforms/utils/chains' import { createEthersProvider } from 'uniswap/src/features/providers/createEthersProvider' diff --git a/packages/uniswap/src/features/portfolio/portfolioUpdates/isInstantTokenBalanceUpdateEnabled.ts b/packages/uniswap/src/features/portfolio/portfolioUpdates/isInstantTokenBalanceUpdateEnabled.ts index f35ff5a208c..12424682f7f 100644 --- a/packages/uniswap/src/features/portfolio/portfolioUpdates/isInstantTokenBalanceUpdateEnabled.ts +++ b/packages/uniswap/src/features/portfolio/portfolioUpdates/isInstantTokenBalanceUpdateEnabled.ts @@ -1,5 +1,4 @@ -import { FeatureFlags, getFeatureFlagName } from 'uniswap/src/features/gating/flags' -import { getStatsigClient } from 'uniswap/src/features/gating/sdk/statsig' +import { FeatureFlags, getFeatureFlagName, getStatsigClient } from '@universe/gating' export function isInstantTokenBalanceUpdateEnabled(): boolean { return getStatsigClient().checkGate(getFeatureFlagName(FeatureFlags.InstantTokenBalanceUpdate)) diff --git a/packages/uniswap/src/features/portfolio/portfolioUpdates/rest/fetchOnChainBalancesRest.test.ts b/packages/uniswap/src/features/portfolio/portfolioUpdates/rest/fetchOnChainBalancesRest.test.ts index 40395fdc848..9054ae0f4e6 100644 --- a/packages/uniswap/src/features/portfolio/portfolioUpdates/rest/fetchOnChainBalancesRest.test.ts +++ b/packages/uniswap/src/features/portfolio/portfolioUpdates/rest/fetchOnChainBalancesRest.test.ts @@ -1,6 +1,6 @@ -import { ApolloClient, NormalizedCacheObject } from '@apollo/client' import { GetPortfolioResponse } from '@uniswap/client-data-api/dist/data/v1/api_pb.d' -import { GraphQLApi } from '@universe/api' +import { type Token as SearchToken } from '@uniswap/client-search/dist/search/v1/api_pb' +import * as searchTokensAndPools from 'uniswap/src/data/rest/searchTokensAndPools' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { fetchOnChainCurrencyBalance } from 'uniswap/src/features/portfolio/api' import { fetchOnChainBalancesRest } from 'uniswap/src/features/portfolio/portfolioUpdates/rest/fetchOnChainBalancesRest' @@ -33,10 +33,19 @@ jest.mock('uniswap/src/features/portfolio/api', () => ({ fetchOnChainCurrencyBalance: jest.fn(), })) +jest.mock('uniswap/src/data/rest/searchTokensAndPools', () => ({ + ...jest.requireActual('uniswap/src/data/rest/searchTokensAndPools'), + fetchTokenByAddress: jest.fn(), +})) + const mockGetOnChainBalancesFetch = fetchOnChainCurrencyBalance as jest.MockedFunction< typeof fetchOnChainCurrencyBalance > +const mockFetchTokenByAddress = searchTokensAndPools.fetchTokenByAddress as jest.MockedFunction< + typeof searchTokensAndPools.fetchTokenByAddress +> + const TEST_ACCOUNT = '0x1234567890123456789012345678901234567890' const TEST_TOKEN_ADDRESS = '0xabcdef0123456789abcdef0123456789abcdef01' const TEST_CHAIN_ID = UniverseChainId.Mainnet @@ -83,10 +92,6 @@ const mockCachedPortfolio = { } as NonNullable describe('fetchOnChainBalancesRest', () => { - const mockApolloClient = { - query: jest.fn(), - } as unknown as ApolloClient - beforeEach(() => { jest.clearAllMocks() }) @@ -100,7 +105,6 @@ describe('fetchOnChainBalancesRest', () => { }) const result = await fetchOnChainBalancesRest({ - apolloClient: mockApolloClient, cachedPortfolio: mockCachedPortfolio, accountAddress: TEST_ACCOUNT, currencyIds: new Set([currencyId]), @@ -149,7 +153,6 @@ describe('fetchOnChainBalancesRest', () => { } as NonNullable const result = await fetchOnChainBalancesRest({ - apolloClient: mockApolloClient, cachedPortfolio: mockCachedPortfolioWithNative, accountAddress: TEST_ACCOUNT, currencyIds: new Set([currencyId]), @@ -172,7 +175,6 @@ describe('fetchOnChainBalancesRest', () => { const invalidCurrencyId = 'invalid-currency-id' const result = await fetchOnChainBalancesRest({ - apolloClient: mockApolloClient, cachedPortfolio: mockCachedPortfolio, accountAddress: TEST_ACCOUNT, currencyIds: new Set([invalidCurrencyId]), @@ -190,32 +192,28 @@ describe('fetchOnChainBalancesRest', () => { balance: mockBalance, }) - // Mock GraphQL query for new token - ;(mockApolloClient.query as jest.Mock).mockResolvedValueOnce({ - data: { - token: { - ...mockToken, - address: MOCK_TOKEN_ADDRESS_2, - symbol: 'NEW', - name: 'New Token', - }, - }, - }) + // Mock REST search for new token + mockFetchTokenByAddress.mockResolvedValueOnce({ + chainId: TEST_CHAIN_ID, + address: MOCK_TOKEN_ADDRESS_2, + symbol: 'NEW', + name: 'New Token', + decimals: 18, + logoUrl: '', + feeData: undefined, + safetyLevel: 0, + protectionInfo: undefined, + } as unknown as SearchToken) const result = await fetchOnChainBalancesRest({ - apolloClient: mockApolloClient, cachedPortfolio: mockCachedPortfolio, // doesn't contain new token accountAddress: TEST_ACCOUNT, currencyIds: new Set([currencyId]), }) - expect(mockApolloClient.query).toHaveBeenCalledWith({ - query: GraphQLApi.TokenDocument, - variables: { - chain: 'ETHEREUM', - address: MOCK_TOKEN_ADDRESS_2, - }, - fetchPolicy: 'cache-first', + expect(mockFetchTokenByAddress).toHaveBeenCalledWith({ + chainId: TEST_CHAIN_ID, + address: MOCK_TOKEN_ADDRESS_2, }) const balanceInfo = result.get(currencyId) @@ -225,7 +223,7 @@ describe('fetchOnChainBalancesRest', () => { expect(balanceInfo?.token?.symbol).toBe('NEW') }) - it('skips tokens when GraphQL query fails', async () => { + it('skips tokens when REST token search fails', async () => { const currencyId = buildCurrencyId(TEST_CHAIN_ID, MOCK_TOKEN_ADDRESS_3) const mockBalance = MOCK_BALANCE_1_ETH @@ -233,13 +231,10 @@ describe('fetchOnChainBalancesRest', () => { balance: mockBalance, }) - // Mock GraphQL query to return null token - ;(mockApolloClient.query as jest.Mock).mockResolvedValueOnce({ - data: { token: null }, - }) + // Mock REST search to return null (token not found) + mockFetchTokenByAddress.mockResolvedValueOnce(null) const result = await fetchOnChainBalancesRest({ - apolloClient: mockApolloClient, cachedPortfolio: mockCachedPortfolio, accountAddress: TEST_ACCOUNT, currencyIds: new Set([currencyId]), @@ -284,7 +279,6 @@ describe('fetchOnChainBalancesRest', () => { .mockResolvedValueOnce({ balance: MOCK_BALANCE_2_ETH }) const result = await fetchOnChainBalancesRest({ - apolloClient: mockApolloClient, cachedPortfolio: mockCachedPortfolioMultiple, accountAddress: TEST_ACCOUNT, currencyIds: new Set([currencyId1, currencyId2]), @@ -310,7 +304,6 @@ describe('fetchOnChainBalancesRest', () => { .mockRejectedValueOnce(new Error('Network error')) const result = await fetchOnChainBalancesRest({ - apolloClient: mockApolloClient, cachedPortfolio: mockCachedPortfolio, accountAddress: TEST_ACCOUNT, currencyIds: new Set([currencyId1, currencyId2]), @@ -332,7 +325,6 @@ describe('fetchOnChainBalancesRest', () => { // Cached portfolio has 1 token worth $100 const result = await fetchOnChainBalancesRest({ - apolloClient: mockApolloClient, cachedPortfolio: mockCachedPortfolio, accountAddress: TEST_ACCOUNT, currencyIds: new Set([currencyId]), diff --git a/packages/uniswap/src/features/portfolio/portfolioUpdates/rest/fetchOnChainBalancesRest.ts b/packages/uniswap/src/features/portfolio/portfolioUpdates/rest/fetchOnChainBalancesRest.ts index 98837526c9b..580ed5e508a 100644 --- a/packages/uniswap/src/features/portfolio/portfolioUpdates/rest/fetchOnChainBalancesRest.ts +++ b/packages/uniswap/src/features/portfolio/portfolioUpdates/rest/fetchOnChainBalancesRest.ts @@ -1,21 +1,20 @@ -import { ApolloClient, NormalizedCacheObject } from '@apollo/client' import { PartialMessage } from '@bufbuild/protobuf' import { GetPortfolioResponse } from '@uniswap/client-data-api/dist/data/v1/api_pb.d' import { Balance } from '@uniswap/client-data-api/dist/data/v1/types_pb' import { CurrencyAmount, NativeCurrency, Token } from '@uniswap/sdk-core' -import { GraphQLApi, TradingApi } from '@universe/api' +import { TradingApi } from '@universe/api' import { getNativeAddress } from 'uniswap/src/constants/addresses' +import { fetchTokenByAddress, searchTokenToCurrencyInfo } from 'uniswap/src/data/rest/searchTokensAndPools' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { getPrimaryStablecoin } from 'uniswap/src/features/chains/utils' -import { currencyIdToContractInput } from 'uniswap/src/features/dataApi/utils/currencyIdToContractInput' -import { gqlTokenToCurrencyInfo } from 'uniswap/src/features/dataApi/utils/gqlTokenToCurrencyInfo' -import { Platform } from 'uniswap/src/features/platforms/types/Platform' +import { isSVMChain } from 'uniswap/src/features/platforms/utils/chains' import { fetchOnChainCurrencyBalance } from 'uniswap/src/features/portfolio/api' import { DenominatedValue, fetchIndicativeQuote, } from 'uniswap/src/features/portfolio/portfolioUpdates/fetchOnChainBalances' import { getCurrencyAmount, ValueType } from 'uniswap/src/features/tokens/getCurrencyAmount' +import { SolanaToken } from 'uniswap/src/features/tokens/SolanaToken' import { toTradingApiSupportedChainId } from 'uniswap/src/features/transactions/swap/utils/tradingApi' import { CurrencyId } from 'uniswap/src/types/currency' import { areAddressesEqual } from 'uniswap/src/utils/addresses' @@ -28,12 +27,10 @@ const FILE_NAME = 'fetchOnChainBalancesRest.ts' // Fetches real-time onchain balances for multiple currencies and converts them to Balance objects export async function fetchOnChainBalancesRest({ - apolloClient, cachedPortfolio, accountAddress, currencyIds, }: { - apolloClient: ApolloClient cachedPortfolio: NonNullable accountAddress: Address currencyIds: Set @@ -64,7 +61,7 @@ export async function fetchOnChainBalancesRest({ const cachedBalance = findCachedBalance({ cachedPortfolio, chainId, currencyAddress }) const token = cachedBalance?.token - const currencyResult = await resolveCurrency({ token, currencyId, apolloClient }) + const currencyResult = await resolveCurrency({ token, currencyId }) if (!currencyResult) { return @@ -259,49 +256,83 @@ function findCachedBalance({ } return areAddressesEqual({ - addressInput1: { address: balance.token.address, platform: Platform.EVM }, - addressInput2: { address: currencyAddress, platform: Platform.EVM }, + addressInput1: { address: balance.token.address, chainId: balance.token.chainId }, + addressInput2: { address: currencyAddress, chainId }, }) }) } -// Resolves currency metadata from cache or by fetching from GraphQL +function getCurrencyFromCache( + token: Balance['token'], + currencyId: CurrencyId, +): { currency: Token | SolanaToken; tokenInfo: null } | null { + if (!token) { + return null + } + + const currencyAddress = currencyIdToAddress(currencyId) + const chainId = currencyIdToChain(currencyId) + + if (!chainId) { + return null + } + + const currency = isSVMChain(chainId) + ? new SolanaToken(chainId, currencyAddress, token.decimals, token.symbol, token.name) + : new Token(chainId, currencyAddress, token.decimals, token.symbol, token.name) + + return { currency, tokenInfo: null } +} + +async function fetchTokenCurrencyInfo( + chainId: UniverseChainId, + address: string, +): Promise | null> { + const searchToken = await fetchTokenByAddress({ + chainId, + address, + }) + + return searchToken ? searchTokenToCurrencyInfo(searchToken) : null +} + +// Resolves `CurrencyInfo` either from cache or via REST search async function resolveCurrency({ token, currencyId, - apolloClient, }: { token?: Balance['token'] currencyId: CurrencyId - apolloClient: ApolloClient -}): Promise<{ currency: Token; tokenInfo: ReturnType | null } | null> { +}): Promise<{ currency: Token | SolanaToken; tokenInfo: ReturnType | null } | null> { const log = createLogger(FILE_NAME, 'resolveCurrency', '[REST-ITBU]') + // Try cache first if (token) { - const currencyAddress = currencyIdToAddress(currencyId) - const chainId = currencyIdToChain(currencyId) as UniverseChainId - const currency = new Token(chainId, currencyAddress, token.decimals, token.symbol, token.name) - return { currency, tokenInfo: null } + const cached = getCurrencyFromCache(token, currencyId) + if (cached) { + return cached + } } - // For new tokens not in cache, fetch token metadata from GraphQL - // TODO(WALL-7215): migrate this to REST once we have a tokens endpoint - const tokenQuery = await apolloClient.query({ - query: GraphQLApi.TokenDocument, - variables: currencyIdToContractInput(currencyId), - fetchPolicy: 'cache-first', - }) + // For new tokens not in cache, fetch token metadata via REST search + const chainId = currencyIdToChain(currencyId) + const currencyAddress = currencyIdToAddress(currencyId) + + if (!chainId || !currencyAddress) { + log.error(new Error('Invalid currencyId in `resolveCurrency`'), { currencyId }) + return null + } - const tokenInfo = tokenQuery.data.token ? gqlTokenToCurrencyInfo(tokenQuery.data.token) : null + const tokenInfo = await fetchTokenCurrencyInfo(chainId, currencyAddress) if (tokenInfo?.currency.isToken) { - log.debug('Fetched token metadata from GraphQL', { + log.debug('Fetched Token via REST Search', { currencyId, currency: tokenInfo.currency, }) return { currency: tokenInfo.currency, tokenInfo } } else { - log.warn('Could not fetch token metadata, skipping asset', { currencyId }) + log.warn('Failed to fetch Token via REST search', { currencyId }) return null } } diff --git a/packages/uniswap/src/features/portfolio/portfolioUpdates/rest/refetchRestQueriesViaOnchainOverrideVariantSaga.ts b/packages/uniswap/src/features/portfolio/portfolioUpdates/rest/refetchRestQueriesViaOnchainOverrideVariantSaga.ts index ca42115f47a..06d15b5e6b1 100644 --- a/packages/uniswap/src/features/portfolio/portfolioUpdates/rest/refetchRestQueriesViaOnchainOverrideVariantSaga.ts +++ b/packages/uniswap/src/features/portfolio/portfolioUpdates/rest/refetchRestQueriesViaOnchainOverrideVariantSaga.ts @@ -147,12 +147,10 @@ export function mergeOnChainBalances( } export async function fetchAndMergeOnchainBalances({ - apolloClient, cachedPortfolio, accountAddress, currencyIds, }: { - apolloClient: ApolloClient cachedPortfolio: Portfolio accountAddress: string currencyIds: Set @@ -165,7 +163,6 @@ export async function fetchAndMergeOnchainBalances({ try { const onchainBalancesByCurrencyId = await fetchOnChainBalancesRest({ - apolloClient, cachedPortfolio, accountAddress, currencyIds, @@ -206,7 +203,8 @@ export function* refetchRestQueriesViaOnchainOverrideVariant({ }: { transaction: TransactionDetails activeAddress: string | null - apolloClient: ApolloClient + // Only pass `null` for Solana where we don't need to refetch GQL queries + apolloClient: ApolloClient | null }): Generator { const currenciesWithBalanceToUpdate = getCurrenciesToUpdate(transaction, activeAddress) @@ -243,7 +241,6 @@ export function* refetchRestQueriesViaOnchainOverrideVariant({ yield* all( portfolioQueriesToUpdate.map((query) => call(updatePortfolioCache, { - apolloClient, ownerAddress: activeAddress, currencyIds: currenciesWithBalanceToUpdate, queryKey: query.queryKey, @@ -255,7 +252,11 @@ export function* refetchRestQueriesViaOnchainOverrideVariant({ yield* delay(REFETCH_DELAY) // Once NFTs are migrated to REST we won't need to do this - yield* call([apolloClient, apolloClient.refetchQueries], { include: [GQLQueries.NftsTab] }) + if (apolloClient) { + yield* call([apolloClient, apolloClient.refetchQueries], { include: [GQLQueries.NftsTab] }) + } else { + log.debug(`Ignoring NFT GQL refetch for ${platform} because apolloClient is null`) + } // Invalidate all portfolio queries that match this address yield* call([SharedQueryClient, SharedQueryClient.invalidateQueries], { @@ -265,12 +266,10 @@ export function* refetchRestQueriesViaOnchainOverrideVariant({ } function* updatePortfolioCache({ - apolloClient, ownerAddress, currencyIds, queryKey, }: { - apolloClient: ApolloClient ownerAddress: string currencyIds: Set queryKey: readonly unknown[] @@ -286,7 +285,6 @@ function* updatePortfolioCache({ } const mergedData = yield* call(fetchAndMergeOnchainBalances, { - apolloClient, cachedPortfolio: cachedPortfolioData.portfolio, accountAddress: ownerAddress, currencyIds, diff --git a/packages/uniswap/src/features/providers/rpcUrlSelector.ts b/packages/uniswap/src/features/providers/rpcUrlSelector.ts index 2a55c4f0abd..ec69efbbdb5 100644 --- a/packages/uniswap/src/features/providers/rpcUrlSelector.ts +++ b/packages/uniswap/src/features/providers/rpcUrlSelector.ts @@ -1,7 +1,6 @@ +import { Experiments, getExperimentValue, PrivateRpcProperties } from '@universe/gating' import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' import { RPCType, UniverseChainId } from 'uniswap/src/features/chains/types' -import { Experiments, PrivateRpcProperties } from 'uniswap/src/features/gating/experiments' -import { getExperimentValue } from 'uniswap/src/features/gating/hooks' import { DEFAULT_FLASHBOTS_ENABLED, FLASHBOTS_DEFAULT_REFUND_PERCENT, diff --git a/packages/uniswap/src/features/search/SearchHistoryResult.ts b/packages/uniswap/src/features/search/SearchHistoryResult.ts index c1bf7bb3b4e..7c26f6bde7f 100644 --- a/packages/uniswap/src/features/search/SearchHistoryResult.ts +++ b/packages/uniswap/src/features/search/SearchHistoryResult.ts @@ -1,7 +1,7 @@ /* * Represents the search result types that are saved in Redux. */ -import { ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' +import { ProtocolVersion } from '@uniswap/client-data-api/dist/data/v1/poolTypes_pb' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { CurrencyId } from 'uniswap/src/types/currency' diff --git a/packages/uniswap/src/features/search/SearchModal/analytics/analytics.ts b/packages/uniswap/src/features/search/SearchModal/analytics/analytics.ts index f19b7f817d6..8af105061e1 100644 --- a/packages/uniswap/src/features/search/SearchModal/analytics/analytics.ts +++ b/packages/uniswap/src/features/search/SearchModal/analytics/analytics.ts @@ -1,4 +1,4 @@ -import { ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' +import { ProtocolVersion } from '@uniswap/client-data-api/dist/data/v1/poolTypes_pb' import { OnchainItemListOptionType, SearchModalOption } from 'uniswap/src/components/lists/items/types' import { extractDomain } from 'uniswap/src/components/lists/items/wallets/utils' import { OnchainItemSection, OnchainItemSectionName } from 'uniswap/src/components/lists/OnchainItemList/types' diff --git a/packages/uniswap/src/features/search/SearchModal/hooks/useFilterCallbacks.ts b/packages/uniswap/src/features/search/SearchModal/hooks/useFilterCallbacks.ts index 40b45cb94fa..b27037d22b3 100644 --- a/packages/uniswap/src/features/search/SearchModal/hooks/useFilterCallbacks.ts +++ b/packages/uniswap/src/features/search/SearchModal/hooks/useFilterCallbacks.ts @@ -1,10 +1,9 @@ -import { useCallback, useEffect, useState } from 'react' -import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { useCallback, useEffect, useMemo, useState } from 'react' import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { UniverseChainId } from 'uniswap/src/features/chains/types' -import { isTestnetChain } from 'uniswap/src/features/chains/utils' import { ModalNameType, WalletEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' +import { parseChainFromTokenSearchQuery } from 'uniswap/src/utils/search/parseChainFromTokenSearchQuery' export function useFilterCallbacks( chainId: UniverseChainId | null, @@ -19,9 +18,7 @@ export function useFilterCallbacks( onChangeText: (newSearchFilter: string) => void } { const [chainFilter, setChainFilter] = useState(chainId) - const [parsedChainFilter, setParsedChainFilter] = useState(null) const [searchFilter, setSearchFilter] = useState(null) - const [parsedSearchFilter, setParsedSearchFilter] = useState(null) const { chains: enabledChains } = useEnabledChains() @@ -29,44 +26,16 @@ export function useFilterCallbacks( // i.e "eth dai" or "dai eth" // parsedChainFilter: 1 // parsedSearchFilter: "dai" - useEffect(() => { - const sanitizedSearch = searchFilter?.trim().replace(' ', ' ') - const splitSearch = sanitizedSearch?.split(' ') - if (!splitSearch || splitSearch.length < 2) { - setParsedChainFilter(null) - setParsedSearchFilter(null) - return - } - - const firstWord = splitSearch[0]?.toLowerCase() - const lastWord = splitSearch[splitSearch.length - 1]?.toLowerCase() - - const firstWordChainMatch = firstWord ? getMatchingChainId(firstWord, enabledChains) : undefined - const lastWordChainMatch = lastWord ? getMatchingChainId(lastWord, enabledChains) : undefined - - if (!chainFilter && firstWordChainMatch) { - // First word is chain, rest is search term - const search = splitSearch.slice(1).join(' ') - if (search) { - setParsedChainFilter(firstWordChainMatch) - setParsedSearchFilter(search) - return + const { chainFilter: parsedChainFilter, searchTerm: parsedSearchFilter } = useMemo(() => { + // If there's already a chain filter set, don't parse chains from search text + if (chainFilter) { + return { + chainFilter: null, + searchTerm: null, } } - - if (!chainFilter && lastWordChainMatch && !firstWordChainMatch) { - // Last word is chain, preceding words are search term - const search = splitSearch.slice(0, -1).join(' ') - if (search) { - setParsedChainFilter(lastWordChainMatch) - setParsedSearchFilter(search) - return - } - } - - setParsedChainFilter(null) - setParsedSearchFilter(null) - }, [searchFilter, chainFilter, enabledChains]) + return parseChainFromTokenSearchQuery(searchFilter, enabledChains) + }, [chainFilter, searchFilter, enabledChains]) useEffect(() => { setChainFilter(chainId) @@ -99,38 +68,3 @@ export function useFilterCallbacks( onChangeText, } } - -/** - * Finds a matching chain ID based on the provided chain name. - * - * @param maybeChainName - The potential chain name to match against - * @param enabledChains - Array of enabled chain IDs to search within - * @returns The matching UniverseChainId or undefined if no match found - */ -const getMatchingChainId = (maybeChainName: string, enabledChains: UniverseChainId[]): UniverseChainId | undefined => { - const lowerCaseChainName = maybeChainName.toLowerCase() - - for (const chainId of enabledChains) { - if (isTestnetChain(chainId)) { - continue - } - - const chainInfo = getChainInfo(chainId) - - // Check against native currency name - const nativeCurrencyName = chainInfo.nativeCurrency.name.toLowerCase() - const firstWord = nativeCurrencyName.split(' ')[0] - - if (firstWord === lowerCaseChainName) { - return chainId - } - - // Check against interface name - const interfaceName = chainInfo.interfaceName.toLowerCase() - if (interfaceName === lowerCaseChainName) { - return chainId - } - } - - return undefined -} diff --git a/packages/uniswap/src/features/settings/hooks.test.ts b/packages/uniswap/src/features/settings/hooks.test.ts index b795e557ff8..0dc873ea549 100644 --- a/packages/uniswap/src/features/settings/hooks.test.ts +++ b/packages/uniswap/src/features/settings/hooks.test.ts @@ -12,7 +12,8 @@ jest.mock('utilities/src/platform', () => ({ ...jest.requireActual('utilities/src/platform'), })) -jest.mock('uniswap/src/features/gating/hooks', () => ({ +jest.mock('@universe/gating', () => ({ + ...jest.requireActual('@universe/gating'), useFeatureFlag: jest.fn(), })) diff --git a/packages/uniswap/src/features/smartWallet/mismatch/MismatchContext.tsx b/packages/uniswap/src/features/smartWallet/mismatch/MismatchContext.tsx index 3f74fc4fe33..f6b554d433c 100644 --- a/packages/uniswap/src/features/smartWallet/mismatch/MismatchContext.tsx +++ b/packages/uniswap/src/features/smartWallet/mismatch/MismatchContext.tsx @@ -1,7 +1,6 @@ +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import React, { createContext, PropsWithChildren, useContext, useMemo } from 'react' import { UniverseChainId } from 'uniswap/src/features/chains/types' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { MismatchAccountEffects } from 'uniswap/src/features/smartWallet/mismatch/MismatchAccountEffects' import type { HasMismatchInput, diff --git a/packages/uniswap/src/features/telemetry/constants/trace/element.ts b/packages/uniswap/src/features/telemetry/constants/trace/element.ts index 963e10245e8..24d4fb4f760 100644 --- a/packages/uniswap/src/features/telemetry/constants/trace/element.ts +++ b/packages/uniswap/src/features/telemetry/constants/trace/element.ts @@ -22,6 +22,7 @@ export enum ElementName { AutorouterVisualizationRow = 'expandable-autorouter-visualization-row', BackButton = 'back-button', BlogLink = 'blog-link', + BridgedAssetsBannerV2 = 'bridged-assets-banner-v2', BridgedAssetTDPSection = 'bridged-asset-tdp-section', BridgeNativeTokenButton = 'bridge-native-token-button', Buy = 'buy', @@ -136,6 +137,7 @@ export enum ElementName { OpenNftsList = 'open-nfts-list', PoolsTableRow = 'pools-table-row', PoolOutOfSyncError = 'pool-out-of-sync-error', + PortfolioNftItem = 'portfolio-nft-item', PreselectAsset = 'preselect-asset', PresetPercentage = 'preset-percentage', PriceUpdateAcceptButton = 'price-update-accept-button', diff --git a/packages/uniswap/src/features/telemetry/constants/trace/section.ts b/packages/uniswap/src/features/telemetry/constants/trace/section.ts index 9a3a749134a..2629576f2d2 100644 --- a/packages/uniswap/src/features/telemetry/constants/trace/section.ts +++ b/packages/uniswap/src/features/telemetry/constants/trace/section.ts @@ -16,6 +16,7 @@ export enum SectionName { ProfileActivityTab = 'profile-activity-tab', ProfileNftsTab = 'profile-nfts-tab', ProfileTokensTab = 'profile-tokens-tab', + PortfolioNftsTab = 'portfolio-nfts-tab', SwapCurrencyInput = 'swap-currency-input', SwapCurrencyOutput = 'swap-currency-output', SwapForm = 'swap-form', diff --git a/packages/uniswap/src/features/telemetry/constants/uniswap.ts b/packages/uniswap/src/features/telemetry/constants/uniswap.ts index 86e3ed35218..b1d890932eb 100644 --- a/packages/uniswap/src/features/telemetry/constants/uniswap.ts +++ b/packages/uniswap/src/features/telemetry/constants/uniswap.ts @@ -1,9 +1,10 @@ export enum UniswapEventName { BalancesReport = 'Balances Report', BalancesReportPerChain = 'Balances Report Per Chain', - TokenSelected = 'Token Selected', - ConversionEventSubmitted = 'Conversion Event Submitted', BlockaidFeesMismatch = 'Blockaid Fees Mismatch', + ConversionEventSubmitted = 'Conversion Event Submitted', + DelegationDetected = 'Delegation Detected', + ExperimentQualifyingEvent = 'Experiment Qualifying Event', LowNetworkTokenInfoModalOpened = 'Low Network Token Info Modal Opened', LpIncentiveCollectRewardsButtonClicked = 'LP Incentive Collect Rewards Button Clicked', LpIncentiveCollectRewardsErrorThrown = 'LP Incentive Collect Rewards Error Thrown', @@ -11,7 +12,7 @@ export enum UniswapEventName { LpIncentiveCollectRewardsSuccess = 'LP Incentive Collect Rewards Success', LpIncentiveLearnMoreCtaClicked = 'LP Incentive Learn More CTA Clicked', SmartWalletMismatchDetected = 'Smart Wallet Mismatch Detected', + TokenSelected = 'Token Selected', TooltipOpened = 'Tooltip Opened', - DelegationDetected = 'Delegation Detected', // alphabetize additional values. } diff --git a/packages/uniswap/src/features/telemetry/types.ts b/packages/uniswap/src/features/telemetry/types.ts index f7cc90ada26..a712650d81c 100644 --- a/packages/uniswap/src/features/telemetry/types.ts +++ b/packages/uniswap/src/features/telemetry/types.ts @@ -7,6 +7,7 @@ import { SharedEventName } from '@uniswap/analytics-events' import { OnChainStatus } from '@uniswap/client-trading/dist/trading/v1/api_pb' import { Currency, TradeType } from '@uniswap/sdk-core' import { TradingApi, UnitagClaimContext } from '@universe/api' +import { Experiments } from '@universe/gating' import type { PresetPercentage } from 'uniswap/src/components/CurrencyInputPanel/AmountInputPresets/types' import { OnchainItemSectionName } from 'uniswap/src/components/lists/OnchainItemList/types' import { UniverseChainId } from 'uniswap/src/features/chains/types' @@ -909,6 +910,9 @@ export type UniverseEventProperties = { delegationAddress: string isActiveChain?: boolean } + [UniswapEventName.ExperimentQualifyingEvent]: { + experiment: Experiments + } [UniswapEventName.BalancesReport]: { total_balances_usd: number wallets: string[] diff --git a/packages/uniswap/src/features/telemetry/utils/logExperimentQualifyingEvent.ts b/packages/uniswap/src/features/telemetry/utils/logExperimentQualifyingEvent.ts new file mode 100644 index 00000000000..983d478aee4 --- /dev/null +++ b/packages/uniswap/src/features/telemetry/utils/logExperimentQualifyingEvent.ts @@ -0,0 +1,9 @@ +import { Experiments } from '@universe/gating' +import { UniswapEventName } from 'uniswap/src/features/telemetry/constants' +import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' + +export function logExperimentQualifyingEvent({ experiment }: { experiment: Experiments }): void { + sendAnalyticsEvent(UniswapEventName.ExperimentQualifyingEvent, { + experiment, + }) +} diff --git a/packages/uniswap/src/features/tokens/hooks/useBlockaidFeeComparisonAnalytics.ts b/packages/uniswap/src/features/tokens/hooks/useBlockaidFeeComparisonAnalytics.ts index 03957097909..54e01673dd8 100644 --- a/packages/uniswap/src/features/tokens/hooks/useBlockaidFeeComparisonAnalytics.ts +++ b/packages/uniswap/src/features/tokens/hooks/useBlockaidFeeComparisonAnalytics.ts @@ -1,8 +1,7 @@ +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { useEffect, useRef } from 'react' import { getNativeAddress } from 'uniswap/src/constants/addresses' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { UniswapEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send.web' import { getTokenProtectionFeeOnTransfer } from 'uniswap/src/features/tokens/safetyUtils' diff --git a/packages/uniswap/src/features/transactions/components/DecimalPadInput/DecimalPad.native.tsx b/packages/uniswap/src/features/transactions/components/DecimalPadInput/DecimalPad.native.tsx index 162c20ab5ed..d94a881af3e 100644 --- a/packages/uniswap/src/features/transactions/components/DecimalPadInput/DecimalPad.native.tsx +++ b/packages/uniswap/src/features/transactions/components/DecimalPadInput/DecimalPad.native.tsx @@ -228,7 +228,7 @@ const KeyButton = memo(function KeyButton({ onPress?.(label, action) scale.value = withSequence(withTiming(1.3, animationOptions), withTiming(1, animationOptions)) opacity.value = withSequence(withTiming(0.75, animationOptions), withTiming(1, animationOptions)) - }, [action, label, onPress, opacity, scale]) + }, [action, label, onPress]) const handleLongPressStart = useCallback((): void => { onLongPressStart?.(label, action) diff --git a/packages/uniswap/src/features/transactions/components/settings/TransactionSettingsModal/TransactionSettingsModalContent/TransactionSettingsRow.tsx b/packages/uniswap/src/features/transactions/components/settings/TransactionSettingsModal/TransactionSettingsModalContent/TransactionSettingsRow.tsx index 7114bd77958..15d71a412e3 100644 --- a/packages/uniswap/src/features/transactions/components/settings/TransactionSettingsModal/TransactionSettingsModalContent/TransactionSettingsRow.tsx +++ b/packages/uniswap/src/features/transactions/components/settings/TransactionSettingsModal/TransactionSettingsModalContent/TransactionSettingsRow.tsx @@ -1,3 +1,4 @@ +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { PropsWithChildren, ReactNode, useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { Flex, Text, TouchableArea } from 'ui/src' @@ -5,8 +6,6 @@ import { InfoCircleFilled } from 'ui/src/components/icons/InfoCircleFilled' import { RotatableChevron } from 'ui/src/components/icons/RotatableChevron' import { iconSizes } from 'ui/src/theme' import { InfoTooltip } from 'uniswap/src/components/tooltip/InfoTooltip' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import type { TransactionSettingConfig } from 'uniswap/src/features/transactions/components/settings/types' interface TransactionSettingRowProps { diff --git a/packages/uniswap/src/features/transactions/components/settings/settingsConfigurations/slippage/useSlippageSettings.ts b/packages/uniswap/src/features/transactions/components/settings/settingsConfigurations/slippage/useSlippageSettings.ts index 8362cfd13cd..b8640e896fb 100644 --- a/packages/uniswap/src/features/transactions/components/settings/settingsConfigurations/slippage/useSlippageSettings.ts +++ b/packages/uniswap/src/features/transactions/components/settings/settingsConfigurations/slippage/useSlippageSettings.ts @@ -152,7 +152,7 @@ export function useSlippageSettings(params?: SlippageSettingsProps): { setCustomSlippageTolerance(parsedValue) } }, - [updateInputWarning, saveOnBlur, inputShakeX, setCustomSlippageTolerance], + [updateInputWarning, saveOnBlur, setCustomSlippageTolerance], ) const onFocusSlippageInput = useCallback((): void => { diff --git a/packages/uniswap/src/features/transactions/components/settings/types.ts b/packages/uniswap/src/features/transactions/components/settings/types.ts index 06239c3491f..146767f0360 100644 --- a/packages/uniswap/src/features/transactions/components/settings/types.ts +++ b/packages/uniswap/src/features/transactions/components/settings/types.ts @@ -1,5 +1,5 @@ +import type { FeatureFlags } from '@universe/gating' import type { AppTFunction } from 'ui/src/i18n/types' -import type { FeatureFlags } from 'uniswap/src/features/gating/flags' import type { Platform } from 'uniswap/src/features/platforms/types/Platform' import type { FrontendSupportedProtocol } from 'uniswap/src/features/transactions/swap/utils/protocols' diff --git a/packages/uniswap/src/features/transactions/hooks/useGetCanSignPermits.ts b/packages/uniswap/src/features/transactions/hooks/useGetCanSignPermits.ts index 085c09ad749..70292374ffc 100644 --- a/packages/uniswap/src/features/transactions/hooks/useGetCanSignPermits.ts +++ b/packages/uniswap/src/features/transactions/hooks/useGetCanSignPermits.ts @@ -1,6 +1,5 @@ +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { UniverseChainId } from 'uniswap/src/features/chains/types' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { useHasAccountMismatchCallback } from 'uniswap/src/features/smartWallet/mismatch/hooks' import { useEvent } from 'utilities/src/react/hooks' diff --git a/packages/uniswap/src/features/transactions/hooks/useGetSwapDelegationAddress.ts b/packages/uniswap/src/features/transactions/hooks/useGetSwapDelegationAddress.ts index 296c287fa6f..24e77262eea 100644 --- a/packages/uniswap/src/features/transactions/hooks/useGetSwapDelegationAddress.ts +++ b/packages/uniswap/src/features/transactions/hooks/useGetSwapDelegationAddress.ts @@ -1,6 +1,5 @@ +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { UniverseChainId } from 'uniswap/src/features/chains/types' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { useEvent } from 'utilities/src/react/hooks' export function useGetSwapDelegationAddress(): (chainId: UniverseChainId | undefined) => string | undefined { diff --git a/packages/uniswap/src/features/transactions/hooks/usePollingIntervalByChain.ts b/packages/uniswap/src/features/transactions/hooks/usePollingIntervalByChain.ts index d9be83dd85d..0a750000644 100644 --- a/packages/uniswap/src/features/transactions/hooks/usePollingIntervalByChain.ts +++ b/packages/uniswap/src/features/transactions/hooks/usePollingIntervalByChain.ts @@ -1,8 +1,6 @@ +import { DynamicConfigs, FeatureFlags, SwapConfigKey, useDynamicConfigValue, useFeatureFlag } from '@universe/gating' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { isMainnetChainId } from 'uniswap/src/features/chains/utils' -import { DynamicConfigs, SwapConfigKey } from 'uniswap/src/features/gating/configs' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useDynamicConfigValue, useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { ONE_SECOND_MS } from 'utilities/src/time/time' export const AVERAGE_L1_BLOCK_TIME_MS = 12 * ONE_SECOND_MS diff --git a/packages/uniswap/src/features/transactions/liquidity/types.ts b/packages/uniswap/src/features/transactions/liquidity/types.ts index ba9cf518a28..9ed54c94ef5 100644 --- a/packages/uniswap/src/features/transactions/liquidity/types.ts +++ b/packages/uniswap/src/features/transactions/liquidity/types.ts @@ -1,4 +1,4 @@ -import { ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' +import { ProtocolVersion } from '@uniswap/client-data-api/dist/data/v1/poolTypes_pb' import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core' import { TradingApi } from '@universe/api' import { PermitTransaction, PermitTypedData } from 'uniswap/src/features/transactions/swap/types/swapTxAndGasInfo' diff --git a/packages/uniswap/src/features/transactions/steps/types.ts b/packages/uniswap/src/features/transactions/steps/types.ts index 3e3d8b56e5a..6e5ee03bf76 100644 --- a/packages/uniswap/src/features/transactions/steps/types.ts +++ b/packages/uniswap/src/features/transactions/steps/types.ts @@ -1,13 +1,23 @@ +import type { TransactionResponse } from '@ethersproject/abstract-provider' import type { CollectFeesSteps } from 'uniswap/src/features/transactions/liquidity/steps/collectFeesSteps' import type { CollectLpIncentiveRewardsSteps } from 'uniswap/src/features/transactions/liquidity/steps/collectIncentiveRewardsSteps' import type { DecreaseLiquiditySteps } from 'uniswap/src/features/transactions/liquidity/steps/decreaseLiquiditySteps' import type { IncreaseLiquiditySteps } from 'uniswap/src/features/transactions/liquidity/steps/increaseLiquiditySteps' import type { MigrationSteps } from 'uniswap/src/features/transactions/liquidity/steps/migrationSteps' +import { TokenApprovalTransactionStep } from 'uniswap/src/features/transactions/steps/approve' import type { SignTypedDataStepFields } from 'uniswap/src/features/transactions/steps/permit2Signature' +import type { Permit2TransactionStep } from 'uniswap/src/features/transactions/steps/permit2Transaction' +import { TokenRevocationTransactionStep } from 'uniswap/src/features/transactions/steps/revoke' import { WrapTransactionStep } from 'uniswap/src/features/transactions/steps/wrap' +import { ExtractedBaseTradeAnalyticsProperties } from 'uniswap/src/features/transactions/swap/analytics' import type { ClassicSwapSteps } from 'uniswap/src/features/transactions/swap/steps/classicSteps' +import { SwapTransactionStep, SwapTransactionStepAsync } from 'uniswap/src/features/transactions/swap/steps/swap' import type { UniswapXSwapSteps } from 'uniswap/src/features/transactions/swap/steps/uniswapxSteps' +import { SetCurrentStepFn } from 'uniswap/src/features/transactions/swap/types/swapCallback' +import { BridgeTrade, ChainedActionTrade, ClassicTrade } from 'uniswap/src/features/transactions/swap/types/trade' +import { TransactionTypeInfo } from 'uniswap/src/features/transactions/types/transactionDetails' import type { ValidatedTransactionRequest } from 'uniswap/src/features/transactions/types/transactionRequests' +import { AccountDetails } from 'uniswap/src/features/wallet/types/AccountDetails' export enum TransactionStepType { TokenApprovalTransaction = 'TokenApproval', @@ -49,3 +59,42 @@ export interface OnChainTransactionFields { export interface OnChainTransactionFieldsBatched { batchedTxRequests: ValidatedTransactionRequest[] } + +export interface HandleOnChainStepParams { + account: AccountDetails + info: TransactionTypeInfo + step: T + setCurrentStep: SetCurrentStepFn + /** Controls whether the function allow submitting a duplicate tx (a tx w/ identical `info` to another recent/pending tx). Defaults to false. */ + allowDuplicativeTx?: boolean + /** Controls whether the function should throw an error upon interrupt or not, defaults to `false`. */ + ignoreInterrupt?: boolean + /** Controls whether the function should wait to return until after the transaction has confirmed. Defaults to `true`. */ + shouldWaitForConfirmation?: boolean + /** Called when data returned from a submitted transaction differs from data originally sent to the wallet. */ + onModification?: ( + response: Pick, + ) => void | Generator +} + +export interface HandleSignatureStepParams { + account: AccountDetails + step: T + setCurrentStep: SetCurrentStepFn + ignoreInterrupt?: boolean +} + +export type HandleApprovalStepParams = Omit< + HandleOnChainStepParams, + 'info' +> + +export type HandleOnChainPermit2TransactionStep = Omit, 'info'> + +export interface HandleSwapStepParams extends Omit { + step: SwapTransactionStep | SwapTransactionStepAsync + signature?: string + trade: ClassicTrade | BridgeTrade | ChainedActionTrade + analytics: ExtractedBaseTradeAnalyticsProperties + onTransactionHash?: (hash: string) => void +} diff --git a/packages/uniswap/src/features/transactions/swap/components/MaxSlippageRow/SlippageInfo/SlippageInfoCaption.tsx b/packages/uniswap/src/features/transactions/swap/components/MaxSlippageRow/SlippageInfo/SlippageInfoCaption.tsx index 1b27e9022c3..31bc13b6cf9 100644 --- a/packages/uniswap/src/features/transactions/swap/components/MaxSlippageRow/SlippageInfo/SlippageInfoCaption.tsx +++ b/packages/uniswap/src/features/transactions/swap/components/MaxSlippageRow/SlippageInfo/SlippageInfoCaption.tsx @@ -75,7 +75,7 @@ export function SlippageInfoCaption({ : t('swap.settings.slippage.output.message')}{' '} {isWebPlatform && ( - + )} diff --git a/packages/uniswap/src/features/transactions/swap/components/SwapFormButton/hooks/useSwapFormButtonText.ts b/packages/uniswap/src/features/transactions/swap/components/SwapFormButton/hooks/useSwapFormButtonText.ts index 37794aa417e..292ec06cc5f 100644 --- a/packages/uniswap/src/features/transactions/swap/components/SwapFormButton/hooks/useSwapFormButtonText.ts +++ b/packages/uniswap/src/features/transactions/swap/components/SwapFormButton/hooks/useSwapFormButtonText.ts @@ -1,8 +1,7 @@ +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { useTranslation } from 'react-i18next' import { nativeOnChain } from 'uniswap/src/constants/tokens' import { useConnectionStatus } from 'uniswap/src/features/accounts/store/hooks' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { isSVMChain } from 'uniswap/src/features/platforms/utils/chains' import { useIsWebFORNudgeEnabled } from 'uniswap/src/features/providers/webForNudgeProvider' import { useTransactionModalContext } from 'uniswap/src/features/transactions/components/TransactionModal/TransactionModalContext' diff --git a/packages/uniswap/src/features/transactions/swap/components/SwapFormSettings/settingsConfigurations/TradeRoutingPreference/TradeRoutingPreferenceScreen.tsx b/packages/uniswap/src/features/transactions/swap/components/SwapFormSettings/settingsConfigurations/TradeRoutingPreference/TradeRoutingPreferenceScreen.tsx index 96b0727a14e..6a68c8df6a3 100644 --- a/packages/uniswap/src/features/transactions/swap/components/SwapFormSettings/settingsConfigurations/TradeRoutingPreference/TradeRoutingPreferenceScreen.tsx +++ b/packages/uniswap/src/features/transactions/swap/components/SwapFormSettings/settingsConfigurations/TradeRoutingPreference/TradeRoutingPreferenceScreen.tsx @@ -1,4 +1,5 @@ import { TradingApi } from '@universe/api' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import type { TFunction } from 'i18next' import type { ReactNode } from 'react' import { useCallback, useState } from 'react' @@ -16,11 +17,8 @@ import { InfoTooltip } from 'uniswap/src/components/tooltip/InfoTooltip' import WarningIcon from 'uniswap/src/components/warnings/WarningIcon' import { uniswapUrls } from 'uniswap/src/constants/urls' import { useUniswapContextSelector } from 'uniswap/src/contexts/UniswapContext' - import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' import { UniverseChainId } from 'uniswap/src/features/chains/types' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants' import Trace from 'uniswap/src/features/telemetry/Trace' import { TransactionSettingsModalId } from 'uniswap/src/features/transactions/components/settings/stores/TransactionSettingsModalStore/createTransactionSettingsModalStore' diff --git a/packages/uniswap/src/features/transactions/swap/components/UnichainInstantBalanceModal/AnimatedTokenFlip.tsx b/packages/uniswap/src/features/transactions/swap/components/UnichainInstantBalanceModal/AnimatedTokenFlip.tsx index 13fca286d05..21aa4e2fdbf 100644 --- a/packages/uniswap/src/features/transactions/swap/components/UnichainInstantBalanceModal/AnimatedTokenFlip.tsx +++ b/packages/uniswap/src/features/transactions/swap/components/UnichainInstantBalanceModal/AnimatedTokenFlip.tsx @@ -23,7 +23,7 @@ export function AnimatedTokenFlip({ duration: 600, easing: Easing.bezier(0.68, -0.3, 0.265, 1.3), }) - }, [processingState, flipAnimation]) + }, [processingState]) const handleTokenClick = (): void => { setProcessingState((prev) => (prev === 'complete' ? 'processing' : 'complete')) diff --git a/packages/uniswap/src/features/transactions/swap/components/UnichainInstantBalanceModal/GradientContainer.native.tsx b/packages/uniswap/src/features/transactions/swap/components/UnichainInstantBalanceModal/GradientContainer.native.tsx index 3bc29963a45..f477b40bbbc 100644 --- a/packages/uniswap/src/features/transactions/swap/components/UnichainInstantBalanceModal/GradientContainer.native.tsx +++ b/packages/uniswap/src/features/transactions/swap/components/UnichainInstantBalanceModal/GradientContainer.native.tsx @@ -33,7 +33,7 @@ export function GradientContainer({ toTokenColor, children }: GradientContainerP blobT1.value = withRepeat(withTiming(1, cfg), -1, true) blobT2.value = withRepeat(withTiming(1, { ...cfg, duration: 16000 }), -1, true) blobT3.value = withRepeat(withTiming(1, { ...cfg, duration: 7000 }), -1, true) - }, [blobT1, blobT2, blobT3]) + }, []) const blob1 = useAnimatedStyle(() => { const innerT = blobT1.value * Math.PI * 2 diff --git a/packages/uniswap/src/features/transactions/swap/components/UnichainInstantBalanceModal/GradientContainer.web.tsx b/packages/uniswap/src/features/transactions/swap/components/UnichainInstantBalanceModal/GradientContainer.web.tsx index 91e0f69d8a7..dfe2d111a64 100644 --- a/packages/uniswap/src/features/transactions/swap/components/UnichainInstantBalanceModal/GradientContainer.web.tsx +++ b/packages/uniswap/src/features/transactions/swap/components/UnichainInstantBalanceModal/GradientContainer.web.tsx @@ -27,7 +27,7 @@ export function GradientContainer({ toTokenColor, children }: GradientContainerP blobT1.value = withRepeat(withTiming(1, cfg), -1, true) blobT2.value = withRepeat(withTiming(1, { ...cfg, duration: 16000 }), -1, true) blobT3.value = withRepeat(withTiming(1, { ...cfg, duration: 7000 }), -1, true) - }, [blobT1, blobT2, blobT3]) + }, []) const blob1 = useAnimatedStyle(() => { const innerT = blobT1.value * Math.PI * 2 diff --git a/packages/uniswap/src/features/transactions/swap/components/UnichainInstantBalanceModal/hooks/receiptFetching/useReceiptSuccessHandler.ts b/packages/uniswap/src/features/transactions/swap/components/UnichainInstantBalanceModal/hooks/receiptFetching/useReceiptSuccessHandler.ts index df86321ffe6..ff2f988a4f3 100644 --- a/packages/uniswap/src/features/transactions/swap/components/UnichainInstantBalanceModal/hooks/receiptFetching/useReceiptSuccessHandler.ts +++ b/packages/uniswap/src/features/transactions/swap/components/UnichainInstantBalanceModal/hooks/receiptFetching/useReceiptSuccessHandler.ts @@ -3,9 +3,10 @@ import { JsonRpcProvider, TransactionReceipt } from '@ethersproject/providers' import { useCallback } from 'react' import { useDispatch } from 'react-redux' import { useTransactionModalContext } from 'uniswap/src/features/transactions/components/TransactionModal/TransactionModalContext' -import { updateTransaction } from 'uniswap/src/features/transactions/slice' +import { updateTransactionWithoutWatch } from 'uniswap/src/features/transactions/slice' import { getOutputAmountUsingSwapLogAndFormData } from 'uniswap/src/features/transactions/swap/components/UnichainInstantBalanceModal/getOutputAmountFromSwapLogAndFormData.ts/getOutputAmountFromSwapLogAndFormData' import { + logSwapTransactionCompleted, NO_OUTPUT_ERROR, reportOutputAmount, } from 'uniswap/src/features/transactions/swap/components/UnichainInstantBalanceModal/hooks/receiptFetching/utils' @@ -75,16 +76,19 @@ export function useReceiptSuccessHandler(): (params: ReceiptSuccessParams) => Pr // updates if the tx is successful so we know to fallback to the form value updateSwapForm({ instantReceiptFetchTime: methodFetchTime - methodRoundtripTime }) - // TODO(APPS-8546): move to a saga to avoid anti-pattern + // TODO(SWAP-407): move to a saga to avoid anti-pattern const parsedReceipt = receiptFromEthersReceipt(receipt, methodFetchTime) - dispatch( - updateTransaction({ - ...transaction, - receipt: parsedReceipt, - status: TransactionStatus.Success, - ...(isWebApp && { isFlashblockTxWithinThreshold }), - }), - ) + + const updatedTransaction = { + ...transaction, + receipt: parsedReceipt, + status: TransactionStatus.Success, + ...(isWebApp && { isFlashblockTxWithinThreshold }), + } + + dispatch(updateTransactionWithoutWatch(updatedTransaction)) + + logSwapTransactionCompleted(updatedTransaction) // Try to get output amount from transfer logs first const outputAmountFromOutputTransferLog = getOutputAmountUsingOutputTransferLog({ diff --git a/packages/uniswap/src/features/transactions/swap/components/UnichainInstantBalanceModal/hooks/receiptFetching/utils.ts b/packages/uniswap/src/features/transactions/swap/components/UnichainInstantBalanceModal/hooks/receiptFetching/utils.ts index 13e4794ad3d..fe449760247 100644 --- a/packages/uniswap/src/features/transactions/swap/components/UnichainInstantBalanceModal/hooks/receiptFetching/utils.ts +++ b/packages/uniswap/src/features/transactions/swap/components/UnichainInstantBalanceModal/hooks/receiptFetching/utils.ts @@ -1,6 +1,13 @@ import { BigNumber } from '@ethersproject/bignumber' +import { TradeType } from '@uniswap/sdk-core' +import { SwapEventName } from 'uniswap/src/features/telemetry/constants' +import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { TransactionScreen } from 'uniswap/src/features/transactions/components/TransactionModal/TransactionModalContext' +import { getRouteAnalyticsData, tradeRoutingToFillType } from 'uniswap/src/features/transactions/swap/analytics' import { SwapFormState } from 'uniswap/src/features/transactions/swap/stores/swapFormStore/types' +import { SwapEventType, timestampTracker } from 'uniswap/src/features/transactions/swap/utils/SwapEventTimestampTracker' +import { TransactionDetails, TransactionType } from 'uniswap/src/features/transactions/types/transactionDetails' +import { isWebApp } from 'utilities/src/platform' export const NO_OUTPUT_ERROR = 'No output amount found in receipt logs' @@ -43,3 +50,74 @@ export function resetSwapFormAndReturnToForm({ updateSwapForm, setScreen }: Rese }) setScreen(TransactionScreen.Form) } + +/** + * TODO(SWAP-407): do NOT copy this logic when moving to a saga; we should restore the original watcher+logging logic once we make the switch + * Logs swap transaction completion analytics for web app + */ +export function logSwapTransactionCompleted(updatedTransaction: TransactionDetails): void { + if (updatedTransaction.typeInfo.type !== TransactionType.Swap || !updatedTransaction.hash || !isWebApp) { + return + } + + const { hash, chainId, addedTime, from, typeInfo, transactionOriginType, routing, id, receipt } = updatedTransaction + const gasUsed = receipt?.gasUsed + const effectiveGasPrice = receipt?.effectiveGasPrice + const confirmedTime = receipt?.confirmedTime + const includesDelegation = 'options' in updatedTransaction ? updatedTransaction.options.includesDelegation : undefined + const isSmartWalletTransaction = + 'options' in updatedTransaction ? updatedTransaction.options.isSmartWalletTransaction : undefined + + const { + quoteId, + gasUseEstimate, + inputCurrencyId, + outputCurrencyId, + transactedUSDValue, + tradeType, + slippageTolerance, + routeString, + protocol, + simulationFailureReasons, + } = typeInfo + + const baseProperties = { + routing: tradeRoutingToFillType({ routing, indicative: false }), + id, + hash, + transactionOriginType, + address: from, + chain_id: chainId, + added_time: addedTime, + confirmed_time: confirmedTime, + gas_used: gasUsed, + effective_gas_price: effectiveGasPrice, + inputCurrencyId, + outputCurrencyId, + gasUseEstimate, + quoteId, + submitViaPrivateRpc: + 'options' in updatedTransaction ? (updatedTransaction.options.submitViaPrivateRpc ?? false) : undefined, + transactedUSDValue, + tradeType: tradeType === TradeType.EXACT_INPUT ? 'EXACT_INPUT' : 'EXACT_OUTPUT', + slippageTolerance, + route: routeString, + protocol, + simulation_failure_reasons: simulationFailureReasons, + includes_delegation: includesDelegation, + is_smart_wallet_transaction: isSmartWalletTransaction, + ...getRouteAnalyticsData(updatedTransaction), + } + + // Log swap success with time-to-swap tracking + const hasSetSwapSuccess = timestampTracker.hasTimestamp(SwapEventType.FirstSwapSuccess) + const elapsedTime = timestampTracker.setElapsedTime(SwapEventType.FirstSwapSuccess) + + sendAnalyticsEvent(SwapEventName.SwapTransactionCompleted, { + ...baseProperties, + time_to_swap: hasSetSwapSuccess ? undefined : elapsedTime, + time_to_swap_since_first_input: hasSetSwapSuccess + ? undefined + : timestampTracker.getElapsedTime(SwapEventType.FirstSwapSuccess, SwapEventType.FirstSwapAction), + }) +} diff --git a/packages/uniswap/src/features/transactions/swap/form/SwapFormScreen/SwapFormDecimalPad/SwapFormDecimalPad.native.tsx b/packages/uniswap/src/features/transactions/swap/form/SwapFormScreen/SwapFormDecimalPad/SwapFormDecimalPad.native.tsx index db1ca5bebbb..b766c8c4cd8 100644 --- a/packages/uniswap/src/features/transactions/swap/form/SwapFormScreen/SwapFormDecimalPad/SwapFormDecimalPad.native.tsx +++ b/packages/uniswap/src/features/transactions/swap/form/SwapFormScreen/SwapFormDecimalPad/SwapFormDecimalPad.native.tsx @@ -1,10 +1,16 @@ import type { MutableRefObject, RefObject } from 'react' -import { useMemo, useState } from 'react' +import { useCallback, useMemo, useState } from 'react' import type { LayoutChangeEvent, TextInputProps } from 'react-native' import { type ButtonProps, Flex, type FlexProps } from 'ui/src' -import { AmountInputPresets } from 'uniswap/src/components/CurrencyInputPanel/AmountInputPresets/AmountInputPresets' +import { + AmountInputPresets, + PRESET_BUTTON_PROPS, +} from 'uniswap/src/components/CurrencyInputPanel/AmountInputPresets/AmountInputPresets' +import { PresetAmountButton } from 'uniswap/src/components/CurrencyInputPanel/AmountInputPresets/PresetAmountButton' import type { PresetPercentage } from 'uniswap/src/components/CurrencyInputPanel/AmountInputPresets/types' +import { PRESET_PERCENTAGES } from 'uniswap/src/components/CurrencyInputPanel/AmountInputPresets/utils' import { MAX_FIAT_INPUT_DECIMALS } from 'uniswap/src/constants/transactions' +import { ElementName } from 'uniswap/src/features/telemetry/constants' import type { DecimalPadInputRef } from 'uniswap/src/features/transactions/components/DecimalPadInput/DecimalPadInput' import { DecimalPadCalculatedSpaceId, @@ -122,6 +128,21 @@ function SwapFormDecimalPadContent({ setAdditionalElementsHeight(event.nativeEvent.layout.height) }) + const renderPreset = useCallback( + (preset: PresetPercentage) => ( + + ), + [currencyAmounts[CurrencyField.INPUT], currencyBalances[CurrencyField.INPUT], onSetPresetValue], + ) + return ( <> )} diff --git a/packages/uniswap/src/features/transactions/swap/hooks/useIsForFiltersEnabled.ts b/packages/uniswap/src/features/transactions/swap/hooks/useIsForFiltersEnabled.ts index a0c81fe6965..34b8e52f31c 100644 --- a/packages/uniswap/src/features/transactions/swap/hooks/useIsForFiltersEnabled.ts +++ b/packages/uniswap/src/features/transactions/swap/hooks/useIsForFiltersEnabled.ts @@ -1,5 +1,4 @@ -import { Experiments, ForFiltersProperties } from 'uniswap/src/features/gating/experiments' -import { useExperimentValue } from 'uniswap/src/features/gating/hooks' +import { Experiments, ForFiltersProperties, useExperimentValue } from '@universe/gating' /** * Hook to determine if ForFilters feature should be enabled diff --git a/packages/uniswap/src/features/transactions/swap/hooks/useIsUnichainFlashblocksEnabled.ts b/packages/uniswap/src/features/transactions/swap/hooks/useIsUnichainFlashblocksEnabled.ts index f222c7b8e74..aa88f876b33 100644 --- a/packages/uniswap/src/features/transactions/swap/hooks/useIsUnichainFlashblocksEnabled.ts +++ b/packages/uniswap/src/features/transactions/swap/hooks/useIsUnichainFlashblocksEnabled.ts @@ -1,24 +1,56 @@ -import { UniverseChainId } from 'uniswap/src/features/chains/types' -import { Experiments, Layers, UnichainFlashblocksProperties } from 'uniswap/src/features/gating/experiments' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' +import { TradingApi } from '@universe/api' import { + Experiments, + FeatureFlags, getExperimentValueFromLayer, getFeatureFlag, + Layers, + UnichainFlashblocksProperties, useExperimentValueFromLayer, useFeatureFlag, -} from 'uniswap/src/features/gating/hooks' +} from '@universe/gating' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { shouldShowFlashblocksUI } from 'uniswap/src/features/transactions/swap/components/UnichainInstantBalanceModal/utils' import { isWebApp } from 'utilities/src/platform' /** - * Hook to determine if Unichain flashblocks feature should be enabled + * Core logic to determine if Flashblocks modal should be enabled. * Returns true only when: * 1. The UnichainFlashblocks feature flag is enabled - * 2. The UnichainFlashblocksModal experiment is enabled (via SwapPage layer) - only on interface - * 3. The current chain is Unichain mainnet or Unichain sepolia + * 2. The user is allocated to the UnichainFlashblocksModal experiment in the SwapPage layer (web only) + * 3. The flashblocksModalEnabled parameter is true for that experiment + * 4. The current chain is Unichain mainnet or Unichain sepolia + */ +function isFlashblocksModalEnabledForChain({ + flashblocksFlagEnabled, + flashblocksModalEnabled, + chainId, +}: { + flashblocksFlagEnabled: boolean + flashblocksModalEnabled: boolean + chainId?: UniverseChainId +}): boolean { + // Check feature flag on all platforms + if (!flashblocksFlagEnabled) { + return false + } + + // Only check experiment on the web app + if (isWebApp && !flashblocksModalEnabled) { + return false + } + + return chainId === UniverseChainId.Unichain || chainId === UniverseChainId.UnichainSepolia +} + +/** + * Hook to determine if the Flashblocks modal should be enabled. + * Uses React hooks to read feature flags and experiments. */ export function useIsUnichainFlashblocksEnabled(chainId?: UniverseChainId): boolean { - const flashblocksFlag = useFeatureFlag(FeatureFlags.UnichainFlashblocks) - const flashblocksExperiment = useExperimentValueFromLayer< + const flashblocksFlagEnabled = useFeatureFlag(FeatureFlags.UnichainFlashblocks) + + const flashblocksModalEnabled = useExperimentValueFromLayer< Layers.SwapPage, Experiments.UnichainFlashblocksModal, boolean @@ -28,29 +60,17 @@ export function useIsUnichainFlashblocksEnabled(chainId?: UniverseChainId): bool defaultValue: false, }) - // Check feature flag on all platforms - if (!flashblocksFlag) { - return false - } - - // Only check experiment on interface platform - if (isWebApp && !flashblocksExperiment) { - return false - } - - return chainId === UniverseChainId.Unichain || chainId === UniverseChainId.UnichainSepolia + return isFlashblocksModalEnabledForChain({ flashblocksFlagEnabled, flashblocksModalEnabled, chainId }) } /** - * Sync function to check if Unichain flashblocks feature is enabled - * Returns true only when: - * 1. The UnichainFlashblocks feature flag is enabled - * 2. The UnichainFlashblocksModal experiment is enabled (via SwapPage layer) - only on interface - * 3. The current chain is Unichain mainnet or Unichain sepolia + * Sync function to determine if the Flashblocks modal should be enabled. + * Uses direct getters to read feature flags and experiments. */ export function getIsFlashblocksEnabled(chainId?: UniverseChainId): boolean { - const flashblocksFlag = getFeatureFlag(FeatureFlags.UnichainFlashblocks) - const flashblocksExperiment = getExperimentValueFromLayer< + const flashblocksFlagEnabled = getFeatureFlag(FeatureFlags.UnichainFlashblocks) + + const flashblocksModalEnabled = getExperimentValueFromLayer< Layers.SwapPage, Experiments.UnichainFlashblocksModal, boolean @@ -60,15 +80,56 @@ export function getIsFlashblocksEnabled(chainId?: UniverseChainId): boolean { defaultValue: false, }) - // Check feature flag on all platforms - if (!flashblocksFlag) { - return false + return isFlashblocksModalEnabledForChain({ flashblocksFlagEnabled, flashblocksModalEnabled, chainId }) +} + +export function getFlashblocksExperimentStatus({ + chainId, + routing, +}: { + chainId?: UniverseChainId + routing?: TradingApi.Routing +}): { + /** Whether to log a qualifying event (swap is eligible) */ + shouldLogQualifyingEvent: boolean + /** Whether to show the flashblocks modal (treatment variant) */ + shouldShowModal: boolean +} { + // Skip routes are not part of the experiment + if (!shouldShowFlashblocksUI(routing)) { + return { shouldLogQualifyingEvent: false, shouldShowModal: false } } - // Only check experiment on interface platform - if (isWebApp && !flashblocksExperiment) { - return false + const flashblocksFlagEnabled = getFeatureFlag(FeatureFlags.UnichainFlashblocks) + const isUnichainChain = chainId === UniverseChainId.Unichain || chainId === UniverseChainId.UnichainSepolia + + if (!flashblocksFlagEnabled || !isUnichainChain) { + return { shouldLogQualifyingEvent: false, shouldShowModal: false } } - return chainId === UniverseChainId.Unichain || chainId === UniverseChainId.UnichainSepolia + // Mobile/Extension: no experiment, feature flag controls behavior + if (!isWebApp) { + return { shouldLogQualifyingEvent: false, shouldShowModal: true } + } + + // Web: experiment controls behavior + const flashblocksModalEnabled = getExperimentValueFromLayer< + Layers.SwapPage, + Experiments.UnichainFlashblocksModal, + boolean + >({ + layerName: Layers.SwapPage, + param: UnichainFlashblocksProperties.FlashblocksModalEnabled, + defaultValue: false, + }) + + return { + // TRUE for all users that reach this point, even if they're not part of the experiment. + // Statsig will later filter out non-allocated users because it applies the auto-exposure filter first, + // and then filters by users that triggered this event *after* being exposed to the experiment. + // More info: https://docs.statsig.com/statsig-warehouse-native/configuration/qualifying-events + shouldLogQualifyingEvent: true, + // TRUE for treatment variant or forced override + shouldShowModal: flashblocksModalEnabled === true, + } } diff --git a/packages/uniswap/src/features/transactions/swap/hooks/useNeedsBridgedAssetWarning.ts b/packages/uniswap/src/features/transactions/swap/hooks/useNeedsBridgedAssetWarning.ts index 1738fa4984d..9569aea3b84 100644 --- a/packages/uniswap/src/features/transactions/swap/hooks/useNeedsBridgedAssetWarning.ts +++ b/packages/uniswap/src/features/transactions/swap/hooks/useNeedsBridgedAssetWarning.ts @@ -1,5 +1,4 @@ import { useMemo } from 'react' -import { checkIsBridgedAsset } from 'uniswap/src/components/BridgedAsset/utils' import { TradeableAsset } from 'uniswap/src/entities/assets' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' import { useDismissedBridgedAssetWarnings } from 'uniswap/src/features/tokens/slice/hooks' @@ -38,19 +37,14 @@ export function useNeedsBridgedAssetWarning( outputCurrencyId && prefilledCurrencies?.some((currency) => currencyId(currency).toLowerCase() === outputCurrencyId.toLowerCase()) - if ( - inputCurrencyInfo && - !inputTokenWarningPreviouslyDismissed && - isInputPrefilled && - checkIsBridgedAsset(inputCurrencyInfo) - ) { + if (inputCurrencyInfo && !inputTokenWarningPreviouslyDismissed && isInputPrefilled && inputCurrencyInfo.isBridged) { tokens.push(inputCurrencyInfo) } if ( outputCurrencyInfo && !outputTokenWarningPreviouslyDismissed && isOutputPrefilled && - checkIsBridgedAsset(outputCurrencyInfo) + outputCurrencyInfo.isBridged ) { tokens.push(outputCurrencyInfo) } diff --git a/packages/uniswap/src/features/transactions/swap/hooks/usePriceUXEnabled.ts b/packages/uniswap/src/features/transactions/swap/hooks/usePriceUXEnabled.ts index a6ae0d406b3..cd339b827d5 100644 --- a/packages/uniswap/src/features/transactions/swap/hooks/usePriceUXEnabled.ts +++ b/packages/uniswap/src/features/transactions/swap/hooks/usePriceUXEnabled.ts @@ -1,5 +1,4 @@ -import { Experiments, Layers, PriceUxUpdateProperties } from 'uniswap/src/features/gating/experiments' -import { useExperimentValueFromLayer } from 'uniswap/src/features/gating/hooks' +import { Experiments, Layers, PriceUxUpdateProperties, useExperimentValueFromLayer } from '@universe/gating' export function usePriceUXEnabled(): boolean { const expValueFromLayer = useExperimentValueFromLayer({ diff --git a/packages/uniswap/src/features/transactions/swap/plan/planSaga.ts b/packages/uniswap/src/features/transactions/swap/plan/planSaga.ts new file mode 100644 index 00000000000..c3c8351dbcf --- /dev/null +++ b/packages/uniswap/src/features/transactions/swap/plan/planSaga.ts @@ -0,0 +1,243 @@ +import { Currency, CurrencyAmount } from '@uniswap/sdk-core' +import { PlanStepStatus, TradingApi } from '@universe/api' +import { call, delay, SagaGenerator } from 'typed-redux-saga' +import { TradingApiClient } from 'uniswap/src/data/apiClients/tradingApi/TradingApiClient' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { UnexpectedTransactionStateError } from 'uniswap/src/features/transactions/errors' +import type { + HandleApprovalStepParams, + HandleSignatureStepParams, + HandleSwapStepParams, +} from 'uniswap/src/features/transactions/steps/types' +import { TransactionStep, TransactionStepType } from 'uniswap/src/features/transactions/steps/types' +import { ExtractedBaseTradeAnalyticsProperties } from 'uniswap/src/features/transactions/swap/analytics' +import { TransactionAndPlanStep, transformSteps } from 'uniswap/src/features/transactions/swap/plan/planStepTransformer' +import { findFirstActionableStep, stepHasFinalized } from 'uniswap/src/features/transactions/swap/plan/utils' +import { SetCurrentStepFn } from 'uniswap/src/features/transactions/swap/types/swapCallback' +import { ValidatedSwapTxContext } from 'uniswap/src/features/transactions/swap/types/swapTxAndGasInfo' +import { isChained, requireRouting } from 'uniswap/src/features/transactions/swap/utils/routing' +import { SignerMnemonicAccountDetails } from 'uniswap/src/features/wallet/types/AccountDetails' +import { createSaga } from 'uniswap/src/utils/saga' +import { logger } from 'utilities/src/logger/logger' + +type SwapParams = { + selectChain: (chainId: number) => Promise + startChainId?: number + account: SignerMnemonicAccountDetails + analytics: ExtractedBaseTradeAnalyticsProperties + swapTxContext: ValidatedSwapTxContext + setCurrentStep: SetCurrentStepFn + setSteps: (steps: TransactionStep[]) => void + getOnPressRetry: (error: Error | undefined) => (() => void) | undefined + disableOneClickSwap: () => void + onSuccess: () => void + onFailure: (error?: Error, onPressRetry?: () => void) => void + onTransactionHash?: (hash: string) => void + v4Enabled: boolean +} + +type PlanCalls = { + handleApprovalTransactionStep: (params: HandleApprovalStepParams) => SagaGenerator + handleSwapTransactionStep: (params: HandleSwapStepParams) => SagaGenerator + handleSignatureStep: (params: HandleSignatureStepParams) => SagaGenerator + getDisplayableError: ({ + error, + step, + flow, + }: { + error: Error + step?: TransactionStep + flow?: string + }) => Error | undefined +} + +const MAX_ATTEMPTS = 60 + +/** + * Waits for a the target step to complete by polling the plan for the given planId and targetStepId. + * + * @returns The updated steps or no steps + */ +function* waitForStepCompletion(params: { + chainId: number + tradeId: string + targetStepId: string + currentStepIndex: number + inputAmount: CurrencyAmount +}): SagaGenerator { + const { chainId, tradeId, targetStepId, currentStepIndex, inputAmount } = params + + const pollingInterval = getChainInfo(chainId).tradingApiPollingIntervalMs + let attempt = 0 + + try { + while (attempt < MAX_ATTEMPTS) { + logger.debug('planSaga', 'waitForStepCompletion', 'waiting for step completion', { + currentStepIndex, + attempt, + maxAttempts: MAX_ATTEMPTS, + }) + + const tradeStatusResponse = yield* call(TradingApiClient.getExistingTrade, { tradeId }) + const latestTargetStep = tradeStatusResponse.steps.find((_step) => _step.stepId === targetStepId) + if (!latestTargetStep) { + throw new Error(`Target stepId=${targetStepId} not found in latest plan.`) + } + if (stepHasFinalized(latestTargetStep)) { + return transformSteps(tradeStatusResponse.steps, inputAmount) + } + attempt++ + yield* delay(pollingInterval) + } + throw new Error(`Exceeded ${MAX_ATTEMPTS} attempts waiting for step completion`) + } catch (error) { + logger.error(error, { tags: { file: 'planSaga', function: 'waitForStepCompletion' } }) + throw error + } +} + +/** + * Saga for executing a plan returned from the Trading API. This plan + * includes a list of steps to be executed in sequence in order to execute + * various actions such as a signature, approval, or swap. + * + * If a inputTradeId exists, it will use that existing plan and refresh the + * plan before beginning execution. As steps are executed, the proofs are sent + * to the TAPI to update the plan. As the steps are executed, the plan continues + * to execute the next step until all last step is confirmed. + */ +function* plan(params: SwapParams & PlanCalls) { + const { + account, + setCurrentStep, + setSteps, + swapTxContext, + analytics, + onSuccess, + onFailure, + selectChain, + handleApprovalTransactionStep, + handleSwapTransactionStep, + handleSignatureStep, + getDisplayableError, + } = params + + logger.debug('planSaga', 'plan', '🚨 plan saga started', swapTxContext) + if (!isChained(swapTxContext)) { + onFailure(new Error('Route not enabled for the plan saga')) + return + } + + const { trade, tradeId: inputTradeId } = swapTxContext + + let response + if (!inputTradeId) { + response = yield* call(TradingApiClient.fetchNewTrade, { + quote: swapTxContext.trade.quote.quote, + }) + } else { + response = yield* call(TradingApiClient.updateExistingTrade, { tradeId: inputTradeId, steps: [] }) + } + let steps: TransactionAndPlanStep[] = transformSteps(response.steps, swapTxContext.trade.inputAmount) + const tradeId = response.tradeId + + let currentStepIndex = steps.findIndex((step) => step.status !== PlanStepStatus.COMPLETE) + let currentStep = steps[currentStepIndex] + setSteps(steps) + if (currentStep) { + setCurrentStep({ step: currentStep, accepted: false }) + } + + try { + while (currentStepIndex < steps.length) { + let signature: string | undefined + let hash: string | undefined + + currentStep = steps[currentStepIndex] + const isLastStep = currentStepIndex === steps.length - 1 + + logger.debug('planSaga', 'plan', '🚨 Starting step', currentStep) + + // @ts-expect-error TODO: SWAP-458 - Temporary fix for chainId until fromChainId is finalized + const swapChainId = currentStep?.chainId || currentStep?.fromChainId || currentStep?.txRequest?.chainId + if (swapChainId) { + yield* call(selectChain, swapChainId) + } + + switch (currentStep?.type) { + case TransactionStepType.TokenRevocationTransaction: + case TransactionStepType.TokenApprovalTransaction: { + hash = yield* call(handleApprovalTransactionStep, { account, step: currentStep, setCurrentStep }) + break + } + case TransactionStepType.Permit2Signature: { + signature = yield* call(handleSignatureStep, { account, step: currentStep, setCurrentStep }) + break + } + case TransactionStepType.SwapTransaction: + case TransactionStepType.SwapTransactionAsync: { + requireRouting(trade, [TradingApi.Routing.CLASSIC, TradingApi.Routing.BRIDGE, TradingApi.Routing.CHAINED]) + hash = yield* call(handleSwapTransactionStep, { + account, + signature, + step: currentStep, + setCurrentStep, + trade, + analytics, + allowDuplicativeTx: true, + }) + break + } + default: { + throw new UnexpectedTransactionStateError(`Unexpected step type: ${currentStep?.type}`) + } + } + + if (hash || signature) { + logger.debug('planSaga', 'plan', '🚨 updating existing trade', tradeId, hash, signature) + yield* call(TradingApiClient.updateExistingTrade, { + tradeId, + steps: [{ stepId: currentStep.stepId, proof: { txHash: hash, signature } }], + }) + } else { + throw new Error('No hash or signature found.') + } + + if (isLastStep) { + yield* call(onSuccess) + return + } + + const updatedSteps: TransactionAndPlanStep[] = yield* call(waitForStepCompletion, { + chainId: swapChainId, + tradeId, + targetStepId: currentStep.stepId, + currentStepIndex, + inputAmount: swapTxContext.trade.inputAmount, + }) + logger.debug('planSaga', 'plan', '🚨 updated steps', updatedSteps) + const nextStep = findFirstActionableStep(updatedSteps) + if (nextStep) { + steps = updatedSteps + setSteps(steps) + setCurrentStep({ step: nextStep, accepted: false }) + currentStepIndex = steps.findIndex((s) => s.stepId === nextStep.stepId) + } else { + throw new Error('No next step found') + } + } + } catch (error) { + const displayableError = getDisplayableError({ + error: error instanceof Error ? error : new Error('Unknown error'), + step: currentStep, + }) + if (displayableError) { + logger.error(displayableError, { tags: { file: 'planSaga', function: 'plan' } }) + } + const onPressRetry = params.getOnPressRetry(displayableError) + onFailure(displayableError, onPressRetry) + return + } +} + +export const planSaga = createSaga(plan, 'planSaga') diff --git a/packages/uniswap/src/features/transactions/swap/plan/planStepTransformer.ts b/packages/uniswap/src/features/transactions/swap/plan/planStepTransformer.ts new file mode 100644 index 00000000000..a9b1a620b78 --- /dev/null +++ b/packages/uniswap/src/features/transactions/swap/plan/planStepTransformer.ts @@ -0,0 +1,62 @@ +import { Currency, CurrencyAmount } from '@uniswap/sdk-core' +import { Method, PlanStep } from '@universe/api' +import { createApprovalTransactionStep } from 'uniswap/src/features/transactions/steps/approve' +import { createPermit2SignatureStep } from 'uniswap/src/features/transactions/steps/permit2Signature' +import { TransactionStep } from 'uniswap/src/features/transactions/steps/types' +import { createSwapTransactionStep } from 'uniswap/src/features/transactions/swap/steps/swap' +import { + validatePermitTypeGuard, + validateTransactionRequestTypeGuard, +} from 'uniswap/src/features/transactions/swap/utils/trade' + +const ERC20_APPROVE_TX_PREFIX = '0x095ea7b3' + +export type TransactionAndPlanStep = TransactionStep & PlanStep + +export const transformStep = ( + step: PlanStep, + inputAmount: CurrencyAmount, +): TransactionAndPlanStep | undefined => { + switch (step.method) { + case Method.SIGN_MSG: + if (!validatePermitTypeGuard(step.payload)) { + return undefined + } + return { + ...step, + ...createPermit2SignatureStep(step.payload, inputAmount.currency), + } + case Method.SEND_TX: + if (!validateTransactionRequestTypeGuard(step.payload)) { + return undefined + } + if (step.payload.data?.toString().startsWith(ERC20_APPROVE_TX_PREFIX)) { + const approvalStep = createApprovalTransactionStep({ + txRequest: step.payload, + amountIn: inputAmount, + }) + if (!approvalStep) { + return undefined + } + return { + ...step, + ...approvalStep, + } + } else { + return { + ...step, + ...createSwapTransactionStep(step.payload), + } + } + // TODO: SWAP-433 - Handle send smart wallet transactions + case Method.SEND_CALLS: + default: + return undefined + } +} + +export const transformSteps = (steps: PlanStep[], inputAmount: CurrencyAmount): TransactionAndPlanStep[] => { + return steps + .map((step) => transformStep(step, inputAmount)) + .filter((step): step is TransactionAndPlanStep => step !== undefined) +} diff --git a/packages/uniswap/src/features/transactions/swap/plan/utils.ts b/packages/uniswap/src/features/transactions/swap/plan/utils.ts new file mode 100644 index 00000000000..967bcb6ba6a --- /dev/null +++ b/packages/uniswap/src/features/transactions/swap/plan/utils.ts @@ -0,0 +1,35 @@ +import { PlanStep, PlanStepStatus } from '@universe/api' +import { TransactionAndPlanStep } from 'uniswap/src/features/transactions/swap/plan/planStepTransformer' +import { ValidatedSwapTxContext } from 'uniswap/src/features/transactions/swap/types/swapTxAndGasInfo' +import { isJupiter } from 'uniswap/src/features/transactions/swap/utils/routing' + +/** Switches to the proper chain, if needed. If a chain switch is necessary and it fails, returns success=false. */ +export async function handleSwitchChains(params: { + selectChain: (chainId: number) => Promise + startChainId?: number + swapTxContext: ValidatedSwapTxContext +}): Promise<{ chainSwitchFailed: boolean }> { + const { selectChain, startChainId, swapTxContext } = params + + const swapChainId = swapTxContext.trade.inputAmount.currency.chainId + + if (isJupiter(swapTxContext) || swapChainId === startChainId) { + return { chainSwitchFailed: false } + } + + const chainSwitched = await selectChain(swapChainId) + + return { chainSwitchFailed: !chainSwitched } +} + +export function stepHasFinalized(step: PlanStep): boolean { + return step.status === PlanStepStatus.COMPLETE || step.status === PlanStepStatus.STEP_ERROR +} + +export function findFirstActionableStep(steps: T[]): T | undefined { + return steps.find((step) => step.status === PlanStepStatus.AWAITING_ACTION) +} + +export function allStepsComplete(steps: PlanStep[]): boolean { + return steps.every((step) => step.status === PlanStepStatus.COMPLETE) +} diff --git a/packages/uniswap/src/features/transactions/swap/review/hooks/useCreateSwapReviewCallbacks.tsx b/packages/uniswap/src/features/transactions/swap/review/hooks/useCreateSwapReviewCallbacks.tsx index 000086bee42..92de3528f62 100644 --- a/packages/uniswap/src/features/transactions/swap/review/hooks/useCreateSwapReviewCallbacks.tsx +++ b/packages/uniswap/src/features/transactions/swap/review/hooks/useCreateSwapReviewCallbacks.tsx @@ -95,6 +95,7 @@ export function useCreateSwapReviewCallbacks(ctx: { const onSuccess = useCallback(() => { // For Unichain networks, trigger confirmation and branch to stall+fetch logic (ie handle in component) if (isFlashblocksEnabled && shouldShowConfirmedState) { + resetCurrentStep() updateSwapForm({ isConfirmed: true, isSubmitting: false, @@ -125,7 +126,7 @@ export function useCreateSwapReviewCallbacks(ctx: { setScreen(TransactionScreen.Form) } onClose() - }, [setScreen, updateSwapForm, onClose, isFlashblocksEnabled, shouldShowConfirmedState]) + }, [setScreen, updateSwapForm, onClose, isFlashblocksEnabled, shouldShowConfirmedState, resetCurrentStep]) const onPending = useCallback(() => { // Skip pending UI only for Unichain networks with flashblocks-compatible routes diff --git a/packages/uniswap/src/features/transactions/swap/review/services/swapTxAndGasInfoService/hooks.ts b/packages/uniswap/src/features/transactions/swap/review/services/swapTxAndGasInfoService/hooks.ts index 12ba0034d82..5defe3a6ac2 100644 --- a/packages/uniswap/src/features/transactions/swap/review/services/swapTxAndGasInfoService/hooks.ts +++ b/packages/uniswap/src/features/transactions/swap/review/services/swapTxAndGasInfoService/hooks.ts @@ -1,12 +1,11 @@ import type { UseQueryResult } from '@tanstack/react-query' import { queryOptions, useQuery } from '@tanstack/react-query' import { GasStrategy, TradingApi } from '@universe/api' +import { DynamicConfigs, SwapConfigKey, useDynamicConfigValue } from '@universe/gating' import { useMemo } from 'react' import { useUniswapContext } from 'uniswap/src/contexts/UniswapContext' import type { UniverseChainId } from 'uniswap/src/features/chains/types' import { useActiveGasStrategy } from 'uniswap/src/features/gas/hooks' -import { DynamicConfigs, SwapConfigKey } from 'uniswap/src/features/gating/configs' -import { useDynamicConfigValue } from 'uniswap/src/features/gating/hooks' import type { SwapDelegationInfo } from 'uniswap/src/features/smartWallet/delegation/types' import { useAllTransactionSettings } from 'uniswap/src/features/transactions/components/settings/stores/transactionSettingsStore/useTransactionSettingsStore' import { useV4SwapEnabled } from 'uniswap/src/features/transactions/swap/hooks/useV4SwapEnabled' diff --git a/packages/uniswap/src/features/transactions/swap/stores/swapFormStore/hooks/useDerivedSwapInfo.ts b/packages/uniswap/src/features/transactions/swap/stores/swapFormStore/hooks/useDerivedSwapInfo.ts index a739bf55eda..7339325a376 100644 --- a/packages/uniswap/src/features/transactions/swap/stores/swapFormStore/hooks/useDerivedSwapInfo.ts +++ b/packages/uniswap/src/features/transactions/swap/stores/swapFormStore/hooks/useDerivedSwapInfo.ts @@ -1,10 +1,9 @@ import { TradeType } from '@uniswap/sdk-core' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { useMemo } from 'react' import { useUniswapContextSelector } from 'uniswap/src/contexts/UniswapContext' import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { UniverseChainId } from 'uniswap/src/features/chains/types' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { useOnChainCurrencyBalance } from 'uniswap/src/features/portfolio/api' import { getCurrencyAmount, ValueType } from 'uniswap/src/features/tokens/getCurrencyAmount' import { useCurrencyInfo } from 'uniswap/src/features/tokens/useCurrencyInfo' diff --git a/packages/uniswap/src/features/transactions/swap/stores/swapTxStore/SwapTxStoreContextProvider.tsx b/packages/uniswap/src/features/transactions/swap/stores/swapTxStore/SwapTxStoreContextProvider.tsx index 839d41380c7..601393df715 100644 --- a/packages/uniswap/src/features/transactions/swap/stores/swapTxStore/SwapTxStoreContextProvider.tsx +++ b/packages/uniswap/src/features/transactions/swap/stores/swapTxStore/SwapTxStoreContextProvider.tsx @@ -1,6 +1,5 @@ +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { useEffect, useState } from 'react' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { useSwapTxAndGasInfo as useServiceBasedSwapTxAndGasInfo } from 'uniswap/src/features/transactions/swap/review/services/swapTxAndGasInfoService/hooks' import { useSwapFormStore } from 'uniswap/src/features/transactions/swap/stores/swapFormStore/useSwapFormStore' import { createSwapTxStore } from 'uniswap/src/features/transactions/swap/stores/swapTxStore/createSwapTxStore' diff --git a/packages/uniswap/src/features/transactions/swap/stores/swapTxStore/hooks/useTransactionRequestInfo.test.ts b/packages/uniswap/src/features/transactions/swap/stores/swapTxStore/hooks/useTransactionRequestInfo.test.ts index ca576871813..f640823efd8 100644 --- a/packages/uniswap/src/features/transactions/swap/stores/swapTxStore/hooks/useTransactionRequestInfo.test.ts +++ b/packages/uniswap/src/features/transactions/swap/stores/swapTxStore/hooks/useTransactionRequestInfo.test.ts @@ -25,9 +25,9 @@ jest.mock( useAllTransactionSettings: jest.fn(), }), ) -jest.mock('uniswap/src/features/gating/hooks', () => { +jest.mock('@universe/gating', () => { return { - ...jest.requireActual('uniswap/src/features/gating/hooks'), + ...jest.requireActual('@universe/gating'), useDynamicConfigValue: jest .fn() .mockImplementation(({ defaultValue }: { config: unknown; key: unknown; defaultValue: unknown }) => { diff --git a/packages/uniswap/src/features/transactions/swap/stores/swapTxStore/hooks/useTransactionRequestInfo.ts b/packages/uniswap/src/features/transactions/swap/stores/swapTxStore/hooks/useTransactionRequestInfo.ts index 7649e9a3f63..78f27648f34 100644 --- a/packages/uniswap/src/features/transactions/swap/stores/swapTxStore/hooks/useTransactionRequestInfo.ts +++ b/packages/uniswap/src/features/transactions/swap/stores/swapTxStore/hooks/useTransactionRequestInfo.ts @@ -1,11 +1,9 @@ import { TradingApi } from '@universe/api' +import { DynamicConfigs, SwapConfigKey, useDynamicConfigValue } from '@universe/gating' import { useEffect, useMemo, useRef } from 'react' import { useUniswapContextSelector } from 'uniswap/src/contexts/UniswapContext' import { useTradingApiSwapQuery } from 'uniswap/src/data/apiClients/tradingApi/useTradingApiSwapQuery' - import { useActiveGasStrategy } from 'uniswap/src/features/gas/hooks' -import { DynamicConfigs, SwapConfigKey } from 'uniswap/src/features/gating/configs' -import { useDynamicConfigValue } from 'uniswap/src/features/gating/hooks' import { useAllTransactionSettings } from 'uniswap/src/features/transactions/components/settings/stores/transactionSettingsStore/useTransactionSettingsStore' import { FALLBACK_SWAP_REQUEST_POLL_INTERVAL_MS } from 'uniswap/src/features/transactions/swap/review/services/swapTxAndGasInfoService/constants' import { processUniswapXResponse } from 'uniswap/src/features/transactions/swap/review/services/swapTxAndGasInfoService/uniswapx/utils' diff --git a/packages/uniswap/src/features/transactions/swap/types/trade.ts b/packages/uniswap/src/features/transactions/swap/types/trade.ts index f2c8f0910ef..6433e8dd70b 100644 --- a/packages/uniswap/src/features/transactions/swap/types/trade.ts +++ b/packages/uniswap/src/features/transactions/swap/types/trade.ts @@ -848,6 +848,7 @@ export class ChainedActionTrade { readonly indicative = false readonly tradeType: TradeType = TradeType.EXACT_INPUT readonly deadline: undefined + readonly priceImpact: undefined // depends on trade type. since exact input, max amount in is the input amount readonly maxAmountIn: CurrencyAmount diff --git a/packages/uniswap/src/features/transactions/swap/utils/getIsWebForNudgeEnabled.ts b/packages/uniswap/src/features/transactions/swap/utils/getIsWebForNudgeEnabled.ts index b83f8a7cd6a..7cb7c6fe964 100644 --- a/packages/uniswap/src/features/transactions/swap/utils/getIsWebForNudgeEnabled.ts +++ b/packages/uniswap/src/features/transactions/swap/utils/getIsWebForNudgeEnabled.ts @@ -1,5 +1,4 @@ -import { Experiments, WebFORNudgesProperties } from 'uniswap/src/features/gating/experiments' -import { getExperimentValue } from 'uniswap/src/features/gating/hooks' +import { Experiments, getExperimentValue, WebFORNudgesProperties } from '@universe/gating' import { isWebApp } from 'utilities/src/platform' export function getIsWebFORNudgeEnabled(): boolean { diff --git a/packages/uniswap/src/features/transactions/swap/utils/protocols.test.ts b/packages/uniswap/src/features/transactions/swap/utils/protocols.test.ts index f6e395ff91c..5911aa798f9 100644 --- a/packages/uniswap/src/features/transactions/swap/utils/protocols.test.ts +++ b/packages/uniswap/src/features/transactions/swap/utils/protocols.test.ts @@ -1,8 +1,7 @@ import { TradingApi } from '@universe/api' +import { FeatureFlags, getFeatureFlag } from '@universe/gating' import { createGetSupportedChainId } from 'uniswap/src/features/chains/hooks/useSupportedChainId' import { UniverseChainId } from 'uniswap/src/features/chains/types' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { getFeatureFlag } from 'uniswap/src/features/gating/hooks' import { createGetV4SwapEnabled } from 'uniswap/src/features/transactions/swap/hooks/useV4SwapEnabled' import { createGetProtocolsForChain, @@ -11,7 +10,8 @@ import { FrontendSupportedProtocol, } from 'uniswap/src/features/transactions/swap/utils/protocols' -jest.mock('uniswap/src/features/gating/hooks', () => ({ +jest.mock('@universe/gating', () => ({ + ...jest.requireActual('@universe/gating'), useFeatureFlag: jest.fn(), getFeatureFlag: jest.fn(), })) diff --git a/packages/uniswap/src/features/transactions/swap/utils/protocols.ts b/packages/uniswap/src/features/transactions/swap/utils/protocols.ts index 75006c824f9..8f0fa23647f 100644 --- a/packages/uniswap/src/features/transactions/swap/utils/protocols.ts +++ b/packages/uniswap/src/features/transactions/swap/utils/protocols.ts @@ -1,11 +1,9 @@ import { TradingApi } from '@universe/api' +import { FeatureFlags, getFeatureFlag, useFeatureFlag } from '@universe/gating' import { useMemo } from 'react' import { useUniswapContextSelector } from 'uniswap/src/contexts/UniswapContext' - import { createGetSupportedChainId } from 'uniswap/src/features/chains/hooks/useSupportedChainId' import { UniverseChainId } from 'uniswap/src/features/chains/types' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { getFeatureFlag, useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { createGetV4SwapEnabled, useV4SwapEnabled } from 'uniswap/src/features/transactions/swap/hooks/useV4SwapEnabled' export const DEFAULT_PROTOCOL_OPTIONS = [ diff --git a/packages/uniswap/src/features/transactions/swap/utils/routing.ts b/packages/uniswap/src/features/transactions/swap/utils/routing.ts index 6ffc30ac3cd..7fb00f6f68c 100644 --- a/packages/uniswap/src/features/transactions/swap/utils/routing.ts +++ b/packages/uniswap/src/features/transactions/swap/utils/routing.ts @@ -1,6 +1,6 @@ import { ADDRESS_ZERO } from '@uniswap/v3-sdk' import { TradingApi } from '@universe/api' - +import { UnexpectedTransactionStateError } from 'uniswap/src/features/transactions/errors' import { type SwapTxAndGasInfo } from 'uniswap/src/features/transactions/swap/types/swapTxAndGasInfo' import { type ValidatedTransactionRequest } from 'uniswap/src/features/transactions/types/transactionRequests' @@ -56,6 +56,16 @@ export function getEVMTxRequest(swapTxContext: SwapTxAndGasInfo): ValidatedTrans return swapTxContext.txRequests?.[0] } +/** Asserts that a given object fits a given routing variant. */ +export function requireRouting( + val: V, + routing: readonly T[], +): asserts val is V & { routing: T } { + if (!routing.includes(val.routing as T)) { + throw new UnexpectedTransactionStateError(`Expected routing ${routing}, got ${val.routing}`) + } +} + export const ACROSS_DAPP_INFO = { name: 'Across API', address: ADDRESS_ZERO, diff --git a/packages/uniswap/src/features/transactions/swap/utils/trade.ts b/packages/uniswap/src/features/transactions/swap/utils/trade.ts index 322130dd229..83958b8a754 100644 --- a/packages/uniswap/src/features/transactions/swap/utils/trade.ts +++ b/packages/uniswap/src/features/transactions/swap/utils/trade.ts @@ -208,6 +208,12 @@ export function validateTransactionRequest( return undefined } +export function validateTransactionRequestTypeGuard( + request?: providers.TransactionRequest | null, +): request is ValidatedTransactionRequest { + return !!request?.to && !!request.chainId +} + export function validateTransactionRequests( requests?: providers.TransactionRequest[] | null, ): PopulatedTransactionRequestArray | undefined { @@ -243,6 +249,10 @@ export function validatePermit(permit: TradingApi.NullablePermit | undefined): V return undefined } +export function validatePermitTypeGuard(permit: TradingApi.NullablePermit | undefined): permit is ValidatedPermit { + return !!permit && !!permit.domain && !!permit.types && !!permit.values +} + export function hasTradeType( typeInfo: TransactionTypeInfo, ): typeInfo is ExactInputSwapTransactionInfo | ExactOutputSwapTransactionInfo { diff --git a/packages/uniswap/src/features/transactions/swap/utils/tradingApi.test.ts b/packages/uniswap/src/features/transactions/swap/utils/tradingApi.test.ts index ea189390fa8..4eeb93197d6 100644 --- a/packages/uniswap/src/features/transactions/swap/utils/tradingApi.test.ts +++ b/packages/uniswap/src/features/transactions/swap/utils/tradingApi.test.ts @@ -1,12 +1,13 @@ import { TradingApi } from '@universe/api' +import { useFeatureFlag } from '@universe/gating' import { UniverseChainId } from 'uniswap/src/features/chains/types' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import type { FrontendSupportedProtocol } from 'uniswap/src/features/transactions/swap/utils/protocols' import { useProtocolsForChain } from 'uniswap/src/features/transactions/swap/utils/protocols' import { useQuoteRoutingParams } from 'uniswap/src/features/transactions/swap/utils/tradingApi' import { renderHook } from 'uniswap/src/test/test-utils' -jest.mock('uniswap/src/features/gating/hooks', () => ({ +jest.mock('@universe/gating', () => ({ + ...jest.requireActual('@universe/gating'), useFeatureFlag: jest.fn(), })) jest.mock('uniswap/src/features/transactions/swap/utils/protocols', () => ({ diff --git a/packages/uniswap/src/features/transactions/swap/utils/tradingApi.ts b/packages/uniswap/src/features/transactions/swap/utils/tradingApi.ts index 42610ca00ab..c4e0bd27bc0 100644 --- a/packages/uniswap/src/features/transactions/swap/utils/tradingApi.ts +++ b/packages/uniswap/src/features/transactions/swap/utils/tradingApi.ts @@ -8,12 +8,11 @@ import type { FeeAmount } from '@uniswap/v3-sdk' import { Pool as V3Pool, Route as V3Route } from '@uniswap/v3-sdk' import { Pool as V4Pool, Route as V4Route } from '@uniswap/v4-sdk' import { type ClassicQuoteResponse, type DiscriminatedQuoteResponse, TradingApi } from '@universe/api' +import { DynamicConfigs, getDynamicConfigValue, SwapConfigKey } from '@universe/gating' import { nativeOnChain } from 'uniswap/src/constants/tokens' import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' import type { UniverseChainId } from 'uniswap/src/features/chains/types' import { isUniverseChainId } from 'uniswap/src/features/chains/utils' -import { DynamicConfigs, SwapConfigKey } from 'uniswap/src/features/gating/configs' -import { getDynamicConfigValue } from 'uniswap/src/features/gating/hooks' import { getCurrencyAmount, ValueType } from 'uniswap/src/features/tokens/getCurrencyAmount' import type { Trade } from 'uniswap/src/features/transactions/swap/types/trade' import { diff --git a/packages/uniswap/src/features/unitags/ClaimUnitagContent.tsx b/packages/uniswap/src/features/unitags/ClaimUnitagContent.tsx index a170222c2c2..86e557aceb0 100644 --- a/packages/uniswap/src/features/unitags/ClaimUnitagContent.tsx +++ b/packages/uniswap/src/features/unitags/ClaimUnitagContent.tsx @@ -131,13 +131,7 @@ export function ClaimUnitagContent({ }) return unsubscribe - }, [ - navigationEventConsumer, - showTextInputView, - addressViewOpacity, - unitagInputContainerTranslateY, - focusUnitagTextInput, - ]) + }, [navigationEventConsumer, showTextInputView, focusUnitagTextInput]) const onChangeTextInput = useCallback( (text: string): void => { @@ -196,15 +190,7 @@ export function ClaimUnitagContent({ } }, initialDelay + translateYDuration) }, - [ - onComplete, - onNavigateContinue, - addressViewOpacity, - entryPoint, - unitagAddress, - unitagInputContainerTranslateY, - fontSize, - ], + [onComplete, onNavigateContinue, entryPoint, unitagAddress, fontSize], ) useEffect(() => { diff --git a/packages/uniswap/src/i18n/locales/source/en-US.json b/packages/uniswap/src/i18n/locales/source/en-US.json index aaa13e3aee8..21e6ace68d0 100644 --- a/packages/uniswap/src/i18n/locales/source/en-US.json +++ b/packages/uniswap/src/i18n/locales/source/en-US.json @@ -144,14 +144,15 @@ "bridgedAsset.send.warning.description": "You’re sending a wrapped version of {{currencySymbol}} on {{chainName}}. Sending it to a centralized exchange will result in a permanent loss of funds.", "bridgedAsset.send.warning.title": "Make sure you’re sending to a compatible address", "bridgedAsset.tdp.description": "This is a bridged version of {{currencySymbol}} that is 1:1 backed by native {{currencySymbol}}.", - "bridgedAsset.wormhole.button": "Continue to Wormhole", - "bridgedAsset.wormhole.description": "Continue to the Wormhole portal to bridge your {{currencySymbol}} from {{chainName}} to {{nativeChainName}}.", + "bridgedAsset.wormhole.button": "Continue to {{provider}}", + "bridgedAsset.wormhole.description": "Continue to the {{provider}} portal to bridge your {{currencySymbol}} from {{chainName}} to {{nativeChainName}}.", "bridgedAsset.wormhole.title": "Withdraw {{currencySymbol}} to {{nativeChainName}}", "bridgedAsset.wormhole.toNativeChain": "to {{nativeChainName}}", "bridgedAsset.wormhole.withdrawToNativeChain": "Withdraw to {{nativeChainName}}", "bridging.estimatedTime.minutesAndSeconds": "~{{minutes}}min {{seconds}}s", "bridging.estimatedTime.minutesOnly": "~{{minutes}}min", "bridging.estimatedTime.secondsOnly": "~{{seconds}}s", + "bridgingPopularTokens.banner.description": "DOGE, XRP, XLP — now available on Unichain.", "chart.candlestick": "Candlestick", "chart.error.pools": "Unable to display historical data for the current pool.", "chart.error.tokens": "Unable to display historical data for the current token.", @@ -1437,17 +1438,32 @@ "poolFinder.availablePools.notFound.description": "No matching v2 pools found. Double-check your token selection and ensure you’re connected to the correct wallet.", "pools.explore": "Explore pools", "portfolio.activity.filters.timePeriod.all": "All time", + "portfolio.activity.filters.transactionType.addLiquidity": "Add Liquidity", "portfolio.activity.filters.transactionType.all": "All types", - "portfolio.activity.filters.transactionType.deposits": "Deposits", - "portfolio.activity.filters.transactionType.staking": "Staking", + "portfolio.activity.filters.transactionType.approvals": "Approvals", + "portfolio.activity.filters.transactionType.claimFees": "Claim Fees", + "portfolio.activity.filters.transactionType.createPool": "Create Pool", + "portfolio.activity.filters.transactionType.mints": "Mints", + "portfolio.activity.filters.transactionType.removeLiquidity": "Remove Liquidity", "portfolio.activity.filters.transactionType.swaps": "Swaps", + "portfolio.activity.filters.transactionType.wraps": "Wraps", + "portfolio.activity.table.column.address": "Address", + "portfolio.activity.table.column.amount": "Amount", + "portfolio.activity.table.column.time": "Time", + "portfolio.activity.table.column.type": "Type", "portfolio.activity.title": "Activity", - "portfolio.connectWallet.summary": "Track tokens, pools, and more across {{amount}}+ networks", "portfolio.defi.title": "DeFi", "portfolio.description": "Track your crypto portfolio across all chains and protocols", + "portfolio.disconnected.connectWallet.cta": "Track your portfolio", + "portfolio.disconnected.cta.description": "Track tokens, positions, NFTs and more across {{numNetworks}}+ networks", + "portfolio.disconnected.demoWallet.description": "This is a view-only example. Connect your wallet to manage your portfolio.", + "portfolio.disconnected.demoWallet.title": "Demo wallet", + "portfolio.disconnected.viewYourPortfolio.cta": "to view your portfolio", "portfolio.nfts.title": "NFTs", "portfolio.overview.title": "Overview", "portfolio.title": "Portfolio", + "portfolio.tokens.balance.totalTokens": "{{numTokens}} tokens", + "portfolio.tokens.emptyState": "No tokens", "portfolio.tokens.table.column.allocation": "Allocation", "portfolio.tokens.table.column.balance": "Balance", "portfolio.tokens.table.column.change1d": "1D Change", diff --git a/packages/uniswap/src/i18n/locales/translations/af-ZA.json b/packages/uniswap/src/i18n/locales/translations/af-ZA.json index 40119ecf73d..d714754e9dc 100644 --- a/packages/uniswap/src/i18n/locales/translations/af-ZA.json +++ b/packages/uniswap/src/i18n/locales/translations/af-ZA.json @@ -1675,6 +1675,7 @@ "settings.action.lock": "Sluit beursie", "settings.action.privacy": "Privaatheidsbeleid", "settings.action.terms": "Diensbepalings", + "settings.connectWalletPlatform.warning": "Om Uniswap op {{platform}}te gebruik, koppel aan 'n beursie wat {{platform}}ondersteun.", "settings.footer": "Met liefde gemaak, \nUniswap-span 🦄", "settings.hideSmallBalances": "Versteek klein saldo's", "settings.hideSmallBalances.subtitle": "Saldo's onder 1 USD sal van jou portefeulje weggesteek word.", @@ -1845,7 +1846,7 @@ "smartWallets.postSwapNudge.title.dapp": "Hierdie toepassing ondersteun slim beursies", "smartWallets.unavailableModal.description": "'n Ander beursieverskaffer bestuur nou slim beursie-instellings vir {{displayName}}. Jy kan Uniswap soos normaalweg voortgaan om te gebruik.", "smartWallets.unavailableModal.title": "Slim beursie-kenmerke is nie beskikbaar nie", - "solanaPromo.banner.description": "Ruil Solana-tokens direk op die Uniswap-webtoepassing.", + "solanaPromo.banner.description": "Ruil Solana-tokens direk op Uniswap.", "solanaPromo.banner.title": "Solana is nou beskikbaar", "solanaPromo.modal.connectWallet": "Koppel jou gunsteling Solana-beursie", "solanaPromo.modal.startSwapping.button": "Begin omruil op Solana", diff --git a/packages/uniswap/src/i18n/locales/translations/ar-SA.json b/packages/uniswap/src/i18n/locales/translations/ar-SA.json index 3e628872b83..a10e2e0aef2 100644 --- a/packages/uniswap/src/i18n/locales/translations/ar-SA.json +++ b/packages/uniswap/src/i18n/locales/translations/ar-SA.json @@ -1675,6 +1675,7 @@ "settings.action.lock": "قفل المحفظة", "settings.action.privacy": "سياسة الخصوصية", "settings.action.terms": "شروط الخدمة", + "settings.connectWalletPlatform.warning": "لاستخدام Uniswap على {{platform}}، قم بالاتصال بمحفظة تدعم {{platform}}.", "settings.footer": "صُنع بكل حب، \nفريق Uniswap 🦄", "settings.hideSmallBalances": "إخفاء الأرصدة الصغيرة", "settings.hideSmallBalances.subtitle": "سيتم إخفاء الأرصدة التي تقل عن 1 دولار أمريكي من محفظتك.", @@ -1845,7 +1846,7 @@ "smartWallets.postSwapNudge.title.dapp": "يدعم هذا التطبيق المحافظ الذكية", "smartWallets.unavailableModal.description": "يُدير موفر محفظة آخر إعدادات المحفظة الذكية لـ {{displayName}}. يمكنك الاستمرار في استخدام Uniswap كالمعتاد.", "smartWallets.unavailableModal.title": "ميزات المحفظة الذكية غير متوفرة", - "solanaPromo.banner.description": "قم بتداول رموز Solana مباشرة على تطبيق Uniswap Web App.", + "solanaPromo.banner.description": "قم بتداول رموز Solana مباشرة على Uniswap.", "solanaPromo.banner.title": "سولانا متاحة الآن", "solanaPromo.modal.connectWallet": "قم بتوصيل محفظة Solana المفضلة لديك", "solanaPromo.modal.startSwapping.button": "ابدأ بالتبديل على سولانا", diff --git a/packages/uniswap/src/i18n/locales/translations/ca-ES.json b/packages/uniswap/src/i18n/locales/translations/ca-ES.json index 7bd08ae2841..3fecdb8ce51 100644 --- a/packages/uniswap/src/i18n/locales/translations/ca-ES.json +++ b/packages/uniswap/src/i18n/locales/translations/ca-ES.json @@ -1675,6 +1675,7 @@ "settings.action.lock": "Bloqueja la cartera", "settings.action.privacy": "Política de privacitat", "settings.action.terms": "Termes del servei", + "settings.connectWalletPlatform.warning": "Per utilitzar Uniswap a {{platform}}, connecteu-vos a un moneder que admeti {{platform}}.", "settings.footer": "Fet amb amor, \nUniswap Team 🦄", "settings.hideSmallBalances": "Amaga petits saldos", "settings.hideSmallBalances.subtitle": "Els saldos inferiors a 1 USD s'amagaran de la vostra cartera.", @@ -1845,7 +1846,7 @@ "smartWallets.postSwapNudge.title.dapp": "Aquesta aplicació admet moneders intel·ligents", "smartWallets.unavailableModal.description": "Un proveïdor de moneders diferent ara gestiona la configuració del moneder intel·ligent per a {{displayName}}. Podeu continuar utilitzant Uniswap com sempre.", "smartWallets.unavailableModal.title": "Les funcions de la cartera intel·ligent no estan disponibles", - "solanaPromo.banner.description": "Intercanvia tokens de Solana directament a l'aplicació web Uniswap.", + "solanaPromo.banner.description": "Intercanvia fitxes de Solana directament a Uniswap.", "solanaPromo.banner.title": "Solana ja està disponible", "solanaPromo.modal.connectWallet": "Connecta la teva cartera Solana preferida", "solanaPromo.modal.startSwapping.button": "Comença a intercanviar a Solana", diff --git a/packages/uniswap/src/i18n/locales/translations/da-DK.json b/packages/uniswap/src/i18n/locales/translations/da-DK.json index 6b105874916..f084b805e68 100644 --- a/packages/uniswap/src/i18n/locales/translations/da-DK.json +++ b/packages/uniswap/src/i18n/locales/translations/da-DK.json @@ -1675,6 +1675,7 @@ "settings.action.lock": "Lås pung", "settings.action.privacy": "Fortrolighedspolitik", "settings.action.terms": "Servicevilkår", + "settings.connectWalletPlatform.warning": "For at bruge Uniswap på {{platform}}skal du oprette forbindelse til en tegnebog, der understøtter {{platform}}.", "settings.footer": "Lavet med kærlighed, \nUniswap Team 🦄", "settings.hideSmallBalances": "Skjul små saldi", "settings.hideSmallBalances.subtitle": "Saldi under 1 USD vil blive skjult fra din portefølje.", @@ -1845,7 +1846,7 @@ "smartWallets.postSwapNudge.title.dapp": "Denne app understøtter smarte tegnebøger", "smartWallets.unavailableModal.description": "En anden wallet-udbyder administrerer nu smart wallet-indstillinger for {{displayName}}. Du kan fortsætte med at bruge Uniswap som normalt.", "smartWallets.unavailableModal.title": "Smart wallet-funktioner er ikke tilgængelige", - "solanaPromo.banner.description": "Handl Solana-tokens direkte på Uniswap-webappen.", + "solanaPromo.banner.description": "Handl Solana-tokens direkte på Uniswap.", "solanaPromo.banner.title": "Solana er nu tilgængelig", "solanaPromo.modal.connectWallet": "Tilslut din foretrukne Solana-pung", "solanaPromo.modal.startSwapping.button": "Begynd at bytte på Solana", diff --git a/packages/uniswap/src/i18n/locales/translations/el-GR.json b/packages/uniswap/src/i18n/locales/translations/el-GR.json index 54df5b70ffa..2bae3774d7b 100644 --- a/packages/uniswap/src/i18n/locales/translations/el-GR.json +++ b/packages/uniswap/src/i18n/locales/translations/el-GR.json @@ -1675,6 +1675,7 @@ "settings.action.lock": "Κλείδωμα πορτοφολιού", "settings.action.privacy": "Πολιτική απορρήτου", "settings.action.terms": "Οροι χρήσης", + "settings.connectWalletPlatform.warning": "Για να χρησιμοποιήσετε το Uniswap στο {{platform}}, συνδεθείτε σε ένα πορτοφόλι που υποστηρίζει το {{platform}}.", "settings.footer": "Φτιαγμένο με αγάπη, \nUniswap Team 🦄", "settings.hideSmallBalances": "Απόκρυψη μικρών υπολοίπων", "settings.hideSmallBalances.subtitle": "Υπόλοιπα κάτω του 1 USD θα κρυφτούν από το χαρτοφυλάκιό σας.", @@ -1845,7 +1846,7 @@ "smartWallets.postSwapNudge.title.dapp": "Αυτή η εφαρμογή υποστηρίζει έξυπνα πορτοφόλια", "smartWallets.unavailableModal.description": "Ένας διαφορετικός πάροχος πορτοφολιού διαχειρίζεται πλέον τις ρυθμίσεις έξυπνου πορτοφολιού για το {{displayName}}. Μπορείτε να συνεχίσετε να χρησιμοποιείτε το Uniswap κανονικά.", "smartWallets.unavailableModal.title": "Οι λειτουργίες έξυπνου πορτοφολιού δεν είναι διαθέσιμες", - "solanaPromo.banner.description": "Ανταλλάξτε μάρκες Solana απευθείας στην εφαρμογή Uniswap Web.", + "solanaPromo.banner.description": "Ανταλλάξτε μάρκες Solana απευθείας στο Uniswap.", "solanaPromo.banner.title": "Η Σολάνα είναι τώρα διαθέσιμη", "solanaPromo.modal.connectWallet": "Συνδέστε το αγαπημένο σας πορτοφόλι Solana", "solanaPromo.modal.startSwapping.button": "Ξεκινήστε την ανταλλαγή στο Solana", diff --git a/packages/uniswap/src/i18n/locales/translations/es-ES.json b/packages/uniswap/src/i18n/locales/translations/es-ES.json index 6cd7dbb7db4..d7c2473c9d3 100644 --- a/packages/uniswap/src/i18n/locales/translations/es-ES.json +++ b/packages/uniswap/src/i18n/locales/translations/es-ES.json @@ -152,6 +152,7 @@ "bridging.estimatedTime.minutesAndSeconds": "~{{minutes}} min {{seconds}} s", "bridging.estimatedTime.minutesOnly": "~{{minutes}} min", "bridging.estimatedTime.secondsOnly": "~{{seconds}} s", + "bridgingPopularTokens.banner.description": "DOGE, XRP, XLP — now available on Unichain.", "chart.candlestick": "Gráfico de velas", "chart.error.pools": "No se pueden mostrar los datos históricos del fondo actual.", "chart.error.tokens": "No se pueden mostrar los datos históricos del token actual.", @@ -1437,17 +1438,32 @@ "poolFinder.availablePools.notFound.description": "No se encontraron fondos v2 que coincidan. Vuelve a verificar la selección de tokens y asegúrate de estar conectado a la billetera correcta.", "pools.explore": "Explorar los fondos", "portfolio.activity.filters.timePeriod.all": "Historial", + "portfolio.activity.filters.transactionType.addLiquidity": "Add Liquidity", "portfolio.activity.filters.transactionType.all": "Todos los tipos", - "portfolio.activity.filters.transactionType.deposits": "Depósitos", - "portfolio.activity.filters.transactionType.staking": "Staking", + "portfolio.activity.filters.transactionType.approvals": "Approvals", + "portfolio.activity.filters.transactionType.claimFees": "Claim Fees", + "portfolio.activity.filters.transactionType.createPool": "Create Pool", + "portfolio.activity.filters.transactionType.mints": "Mints", + "portfolio.activity.filters.transactionType.removeLiquidity": "Remove Liquidity", "portfolio.activity.filters.transactionType.swaps": "Intercambios", + "portfolio.activity.filters.transactionType.wraps": "Wraps", + "portfolio.activity.table.column.address": "Address", + "portfolio.activity.table.column.amount": "Amount", + "portfolio.activity.table.column.time": "Time", + "portfolio.activity.table.column.type": "Type", "portfolio.activity.title": "Actividad", - "portfolio.connectWallet.summary": "Track tokens, pools, and more across {{amount}}+ networks", "portfolio.defi.title": "DeFi", "portfolio.description": "Rastrea tu cartera de criptomonedas en todas las cadenas y protocolos", + "portfolio.disconnected.connectWallet.cta": "Track your portfolio", + "portfolio.disconnected.cta.description": "Track tokens, positions, NFTs and more across {{numNetworks}}+ networks", + "portfolio.disconnected.demoWallet.description": "This is a view-only example. Connect your wallet to manage your portfolio.", + "portfolio.disconnected.demoWallet.title": "Demo wallet", + "portfolio.disconnected.viewYourPortfolio.cta": "to view your portfolio", "portfolio.nfts.title": "NFT", "portfolio.overview.title": "Resumen", "portfolio.title": "Cartera", + "portfolio.tokens.balance.totalTokens": "{{numTokens}} tokens", + "portfolio.tokens.emptyState": "No tokens", "portfolio.tokens.table.column.allocation": "Asignación", "portfolio.tokens.table.column.balance": "Saldo", "portfolio.tokens.table.column.change1d": "Variación en las últimas 24 horas", @@ -2505,6 +2521,12 @@ "uwulink.error.insufficientTokens": "No hay suficientes {{tokenSymbol}} en {{chain}}", "v2.notAvailable": "Uniswap V2 no está disponible en esta red.", "wallet.appSignIn": "Ingresar con la app", + "wallet.connecting.description": "Complete connection in your wallet", + "wallet.connecting.solanaPrompt": "Use Solana on Uniswap", + "wallet.connecting.solanaPrompt.button": "Use Solana", + "wallet.connecting.solanaPrompt.description": "You’ll be asked to connect again in MetaMask", + "wallet.connecting.title.evm": "Connect to {{walletName}}", + "wallet.connecting.title.svm": "Connect to {{walletName}} on Solana", "wallet.connectingAgreement": "Si conectas una billetera, aceptas las Condiciones del servicio de Uniswap Labs y consientes en su Política de privacidad.", "wallet.connectionFailed.message": "Connection attempt failed. Please try again, following the steps to connect in your wallet.", "wallet.mismatch.popup.description": "La billetera que tienes conectada no es compatible con algunas funciones.", diff --git a/packages/uniswap/src/i18n/locales/translations/fi-FI.json b/packages/uniswap/src/i18n/locales/translations/fi-FI.json index 659ba918272..9dd03a9a09d 100644 --- a/packages/uniswap/src/i18n/locales/translations/fi-FI.json +++ b/packages/uniswap/src/i18n/locales/translations/fi-FI.json @@ -1675,6 +1675,7 @@ "settings.action.lock": "Lukittava lompakko", "settings.action.privacy": "Tietosuojakäytäntö", "settings.action.terms": "Käyttöehdot", + "settings.connectWalletPlatform.warning": "Käyttääksesi Uniswapia {{platform}}:ssä, muodosta yhteys lompakkoon, joka tukee {{platform}}:ää.", "settings.footer": "Tehty rakkaudella, \nUniswap Team 🦄", "settings.hideSmallBalances": "Piilota pienet saldot", "settings.hideSmallBalances.subtitle": "Alle 1 USD:n saldot piilotetaan salkustasi.", @@ -1845,7 +1846,7 @@ "smartWallets.postSwapNudge.title.dapp": "Tämä sovellus tukee älykkäitä lompakoita", "smartWallets.unavailableModal.description": "Eri lompakkopalveluntarjoaja hallinnoi nyt {{displayName}}:n älylompakkoasetuksia. Voit jatkaa Uniswapin käyttöä normaalisti.", "smartWallets.unavailableModal.title": "Älykäs lompakon ominaisuudet eivät ole käytettävissä", - "solanaPromo.banner.description": "Vaihda Solana-tokeneita suoraan Uniswap-verkkosovelluksessa.", + "solanaPromo.banner.description": "Vaihda Solana-tokeneita suoraan Uniswapissa.", "solanaPromo.banner.title": "Solana on nyt saatavilla", "solanaPromo.modal.connectWallet": "Yhdistä suosikki Solana-lompakkosi", "solanaPromo.modal.startSwapping.button": "Aloita vaihtaminen Solanan kanssa", diff --git a/packages/uniswap/src/i18n/locales/translations/fil-PH.json b/packages/uniswap/src/i18n/locales/translations/fil-PH.json index 323e3c414ee..047dae48f21 100644 --- a/packages/uniswap/src/i18n/locales/translations/fil-PH.json +++ b/packages/uniswap/src/i18n/locales/translations/fil-PH.json @@ -152,6 +152,7 @@ "bridging.estimatedTime.minutesAndSeconds": "~{{minutes}}min {{seconds}}s", "bridging.estimatedTime.minutesOnly": "~{{minutes}}min", "bridging.estimatedTime.secondsOnly": "~{{seconds}}s", + "bridgingPopularTokens.banner.description": "DOGE, XRP, XLP — now available on Unichain.", "chart.candlestick": "Candlestick", "chart.error.pools": "Hindi maipakita ang dating data para sa kasalukuyang pool.", "chart.error.tokens": "Hindi maipakita ang dating data para sa kasalukuyang token.", @@ -1437,17 +1438,32 @@ "poolFinder.availablePools.notFound.description": "Walang nakitang v2 pool na tumutugma. I-double check ang iyong napiling token at tiyaking nakakonekta ka sa tamang wallet.", "pools.explore": "I-explore ang mga pool", "portfolio.activity.filters.timePeriod.all": "Lahat ng oras", + "portfolio.activity.filters.transactionType.addLiquidity": "Add Liquidity", "portfolio.activity.filters.transactionType.all": "Lahat ng uri", - "portfolio.activity.filters.transactionType.deposits": "Mga Deposito", - "portfolio.activity.filters.transactionType.staking": "Staking", + "portfolio.activity.filters.transactionType.approvals": "Approvals", + "portfolio.activity.filters.transactionType.claimFees": "Claim Fees", + "portfolio.activity.filters.transactionType.createPool": "Create Pool", + "portfolio.activity.filters.transactionType.mints": "Mints", + "portfolio.activity.filters.transactionType.removeLiquidity": "Remove Liquidity", "portfolio.activity.filters.transactionType.swaps": "Mga Swap", + "portfolio.activity.filters.transactionType.wraps": "Wraps", + "portfolio.activity.table.column.address": "Address", + "portfolio.activity.table.column.amount": "Amount", + "portfolio.activity.table.column.time": "Time", + "portfolio.activity.table.column.type": "Type", "portfolio.activity.title": "Aktibidad", - "portfolio.connectWallet.summary": "Track tokens, pools, and more across {{amount}}+ networks", "portfolio.defi.title": "DeFi", "portfolio.description": "I-track ang iyong crypto portfolio sa lahat ng chain at protocol", + "portfolio.disconnected.connectWallet.cta": "Track your portfolio", + "portfolio.disconnected.cta.description": "Track tokens, positions, NFTs and more across {{numNetworks}}+ networks", + "portfolio.disconnected.demoWallet.description": "This is a view-only example. Connect your wallet to manage your portfolio.", + "portfolio.disconnected.demoWallet.title": "Demo wallet", + "portfolio.disconnected.viewYourPortfolio.cta": "to view your portfolio", "portfolio.nfts.title": "Mga NFT", "portfolio.overview.title": "Pangkalahatang-ideya", "portfolio.title": "Portfolio", + "portfolio.tokens.balance.totalTokens": "{{numTokens}} tokens", + "portfolio.tokens.emptyState": "No tokens", "portfolio.tokens.table.column.allocation": "Alokasyon", "portfolio.tokens.table.column.balance": "Balanse", "portfolio.tokens.table.column.change1d": "Pagbabago sa 1D", @@ -2505,6 +2521,12 @@ "uwulink.error.insufficientTokens": "Hindi sapat ang {{tokenSymbol}} sa {{chain}}", "v2.notAvailable": "Hindi available ang Uniswap V2 sa network na ito.", "wallet.appSignIn": "Mag-sign in gamit ang app", + "wallet.connecting.description": "Complete connection in your wallet", + "wallet.connecting.solanaPrompt": "Use Solana on Uniswap", + "wallet.connecting.solanaPrompt.button": "Use Solana", + "wallet.connecting.solanaPrompt.description": "You’ll be asked to connect again in MetaMask", + "wallet.connecting.title.evm": "Connect to {{walletName}}", + "wallet.connecting.title.svm": "Connect to {{walletName}} on Solana", "wallet.connectingAgreement": "Sa pamamagitan ng pagkonekta ng wallet, sumasang-ayon ka sa Mga Tuntunin ng Serbisyo ng Uniswap Labs at pumapayag ka sa Patakaran sa Privacy nito.", "wallet.connectionFailed.message": "Connection attempt failed. Please try again, following the steps to connect in your wallet.", "wallet.mismatch.popup.description": "Ang ilang feature ay hindi sinusuportahan ng iyong nakakonektang wallet.", diff --git a/packages/uniswap/src/i18n/locales/translations/fr-FR.json b/packages/uniswap/src/i18n/locales/translations/fr-FR.json index 0110021626c..87a016d4648 100644 --- a/packages/uniswap/src/i18n/locales/translations/fr-FR.json +++ b/packages/uniswap/src/i18n/locales/translations/fr-FR.json @@ -152,6 +152,7 @@ "bridging.estimatedTime.minutesAndSeconds": "Env. {{minutes}} min {{seconds}} s", "bridging.estimatedTime.minutesOnly": "Env. {{minutes}} min", "bridging.estimatedTime.secondsOnly": "Env. {{seconds}} s", + "bridgingPopularTokens.banner.description": "DOGE, XRP, XLP — now available on Unichain.", "chart.candlestick": "Chandelier", "chart.error.pools": "Impossible d'afficher les données historiques du pool actuel.", "chart.error.tokens": "Impossible d'afficher les données historiques du token actuel.", @@ -1437,17 +1438,32 @@ "poolFinder.availablePools.notFound.description": "Aucun pool V2 ne correspond à votre sélection. Vérifiez le(s) token(s) sélectionné(s) et assurez-vous d’être connecté au bon wallet.", "pools.explore": "Découvrir les pools", "portfolio.activity.filters.timePeriod.all": "Toujours", + "portfolio.activity.filters.transactionType.addLiquidity": "Add Liquidity", "portfolio.activity.filters.transactionType.all": "Tous les types", - "portfolio.activity.filters.transactionType.deposits": "Dépôts", - "portfolio.activity.filters.transactionType.staking": "Staking", + "portfolio.activity.filters.transactionType.approvals": "Approvals", + "portfolio.activity.filters.transactionType.claimFees": "Claim Fees", + "portfolio.activity.filters.transactionType.createPool": "Create Pool", + "portfolio.activity.filters.transactionType.mints": "Mints", + "portfolio.activity.filters.transactionType.removeLiquidity": "Remove Liquidity", "portfolio.activity.filters.transactionType.swaps": "Échanges", + "portfolio.activity.filters.transactionType.wraps": "Wraps", + "portfolio.activity.table.column.address": "Address", + "portfolio.activity.table.column.amount": "Amount", + "portfolio.activity.table.column.time": "Time", + "portfolio.activity.table.column.type": "Type", "portfolio.activity.title": "Activité", - "portfolio.connectWallet.summary": "Track tokens, pools, and more across {{amount}}+ networks", "portfolio.defi.title": "DeFi", "portfolio.description": "Suivez votre portefeuille de crypto à travers toutes les chaînes et protocoles", + "portfolio.disconnected.connectWallet.cta": "Track your portfolio", + "portfolio.disconnected.cta.description": "Track tokens, positions, NFTs and more across {{numNetworks}}+ networks", + "portfolio.disconnected.demoWallet.description": "This is a view-only example. Connect your wallet to manage your portfolio.", + "portfolio.disconnected.demoWallet.title": "Demo wallet", + "portfolio.disconnected.viewYourPortfolio.cta": "to view your portfolio", "portfolio.nfts.title": "NFT", "portfolio.overview.title": "Aperçu", "portfolio.title": "Portefeuille", + "portfolio.tokens.balance.totalTokens": "{{numTokens}} tokens", + "portfolio.tokens.emptyState": "No tokens", "portfolio.tokens.table.column.allocation": "Allocation", "portfolio.tokens.table.column.balance": "Solde", "portfolio.tokens.table.column.change1d": "Évolution sur 1 j", @@ -2505,6 +2521,12 @@ "uwulink.error.insufficientTokens": "Pas assez de {{tokenSymbol}} sur {{chain}}", "v2.notAvailable": "Uniswap V2 n'est pas disponible sur ce réseau.", "wallet.appSignIn": "Se connecter avec l'app", + "wallet.connecting.description": "Complete connection in your wallet", + "wallet.connecting.solanaPrompt": "Use Solana on Uniswap", + "wallet.connecting.solanaPrompt.button": "Use Solana", + "wallet.connecting.solanaPrompt.description": "You’ll be asked to connect again in MetaMask", + "wallet.connecting.title.evm": "Connect to {{walletName}}", + "wallet.connecting.title.svm": "Connect to {{walletName}} on Solana", "wallet.connectingAgreement": "En connectant un wallet, vous acceptez les Conditions d'utilisation d'Uniswap Labs et vous consentez à sa Politique de confidentialité.", "wallet.connectionFailed.message": "Connection attempt failed. Please try again, following the steps to connect in your wallet.", "wallet.mismatch.popup.description": "Certaines fonctionnalités ne sont pas prises en charge par votre wallet connecté.", diff --git a/packages/uniswap/src/i18n/locales/translations/he-IL.json b/packages/uniswap/src/i18n/locales/translations/he-IL.json index 5c183c131d7..ece09b33c76 100644 --- a/packages/uniswap/src/i18n/locales/translations/he-IL.json +++ b/packages/uniswap/src/i18n/locales/translations/he-IL.json @@ -1675,6 +1675,7 @@ "settings.action.lock": "מנעול ארנק", "settings.action.privacy": "מדיניות הפרטיות", "settings.action.terms": "תנאי השירות", + "settings.connectWalletPlatform.warning": "כדי להשתמש ב-Uniswap ב- {{platform}}, התחבר לארנק שתומך ב- {{platform}}.", "settings.footer": "מיוצר באהבה, \nצוות Uniswap 🦄", "settings.hideSmallBalances": "הסתר יתרות קטנות", "settings.hideSmallBalances.subtitle": "יתרות מתחת ל-1 USD יוסתרו מהתיק שלך.", @@ -1845,7 +1846,7 @@ "smartWallets.postSwapNudge.title.dapp": "אפליקציה זו תומכת בארנקים חכמים", "smartWallets.unavailableModal.description": "ספק ארנק אחר מנהל כעת את הגדרות הארנק החכם עבור {{displayName}}. ניתן להמשיך להשתמש ב-Uniswap כרגיל.", "smartWallets.unavailableModal.title": "תכונות הארנק החכם אינן זמינות", - "solanaPromo.banner.description": "סחרו באסימוני סולאנה ישירות באפליקציית האינטרנט של Uniswap.", + "solanaPromo.banner.description": "סחרו באסימוני סולאנה ישירות ב-Uniswap.", "solanaPromo.banner.title": "סולאנה זמינה כעת", "solanaPromo.modal.connectWallet": "חבר את ארנק סולאנה המועדף עליך", "solanaPromo.modal.startSwapping.button": "התחל להחליף על סולאנה", diff --git a/packages/uniswap/src/i18n/locales/translations/hi-IN.json b/packages/uniswap/src/i18n/locales/translations/hi-IN.json index ca00c6fb653..8ceab52826a 100644 --- a/packages/uniswap/src/i18n/locales/translations/hi-IN.json +++ b/packages/uniswap/src/i18n/locales/translations/hi-IN.json @@ -1675,6 +1675,7 @@ "settings.action.lock": "बटुआ बंद करो", "settings.action.privacy": "गोपनीयता नीति", "settings.action.terms": "सेवा की शर्तें", + "settings.connectWalletPlatform.warning": "{{platform}}पर Uniswap का उपयोग करने के लिए, {{platform}}का समर्थन करने वाले वॉलेट से कनेक्ट करें।", "settings.footer": "प्यार से बनाया गया, \nUniswap टीम 🦄", "settings.hideSmallBalances": "छोटे-छोटे शेष छिपाएँ", "settings.hideSmallBalances.subtitle": "1 USD से कम शेष राशि आपके पोर्टफोलियो से छिपा दी जाएगी।", @@ -1845,7 +1846,7 @@ "smartWallets.postSwapNudge.title.dapp": "यह ऐप स्मार्ट वॉलेट को सपोर्ट करता है", "smartWallets.unavailableModal.description": "एक अलग वॉलेट प्रदाता अब {{displayName}}के लिए स्मार्ट वॉलेट सेटिंग्स का प्रबंधन कर रहा है। आप सामान्य रूप से Uniswap का उपयोग जारी रख सकते हैं।", "smartWallets.unavailableModal.title": "स्मार्ट वॉलेट सुविधाएँ उपलब्ध नहीं हैं", - "solanaPromo.banner.description": "यूनिस्वैप वेब ऐप पर सीधे सोलाना टोकन का व्यापार करें।", + "solanaPromo.banner.description": "यूनिस्वैप पर सीधे सोलाना टोकन का व्यापार करें।", "solanaPromo.banner.title": "सोलाना अब उपलब्ध है", "solanaPromo.modal.connectWallet": "अपने पसंदीदा सोलाना वॉलेट को कनेक्ट करें", "solanaPromo.modal.startSwapping.button": "सोलाना पर स्वैपिंग शुरू करें", diff --git a/packages/uniswap/src/i18n/locales/translations/hu-HU.json b/packages/uniswap/src/i18n/locales/translations/hu-HU.json index 827d0da4825..3265b8f67ee 100644 --- a/packages/uniswap/src/i18n/locales/translations/hu-HU.json +++ b/packages/uniswap/src/i18n/locales/translations/hu-HU.json @@ -1675,6 +1675,7 @@ "settings.action.lock": "Zárható pénztárca", "settings.action.privacy": "Adatvédelmi irányelvek", "settings.action.terms": "Szolgáltatás feltételei", + "settings.connectWalletPlatform.warning": "A Uniswap {{platform}}tárcán való használatához csatlakozz egy olyan tárcához, amely támogatja a {{platform}}tárcát.", "settings.footer": "Szeretettel készült, \nUniswap Team 🦄", "settings.hideSmallBalances": "Kis egyenlegek elrejtése", "settings.hideSmallBalances.subtitle": "Az 1 USD alatti egyenlegek el lesznek rejtve a portfóliójában.", @@ -1845,7 +1846,7 @@ "smartWallets.postSwapNudge.title.dapp": "Ez az alkalmazás támogatja az intelligens pénztárcákat", "smartWallets.unavailableModal.description": "Egy másik pénztárca-szolgáltató kezeli a {{displayName}}intelligens pénztárca beállításait. A Uniswap szolgáltatást a szokásos módon használhatod.", "smartWallets.unavailableModal.title": "Az intelligens pénztárca funkciói nem érhetők el", - "solanaPromo.banner.description": "Kereskedjen Solana tokenekkel közvetlenül az Uniswap webes alkalmazásban.", + "solanaPromo.banner.description": "Cserélj Solana tokeneket közvetlenül az Uniswap-on.", "solanaPromo.banner.title": "A Solana már elérhető", "solanaPromo.modal.connectWallet": "Csatlakoztassa kedvenc Solana pénztárcáját", "solanaPromo.modal.startSwapping.button": "Kezdj el cserélgetni a Solanán", diff --git a/packages/uniswap/src/i18n/locales/translations/id-ID.json b/packages/uniswap/src/i18n/locales/translations/id-ID.json index 7eb5c98e8ea..61ac7b19cc6 100644 --- a/packages/uniswap/src/i18n/locales/translations/id-ID.json +++ b/packages/uniswap/src/i18n/locales/translations/id-ID.json @@ -152,6 +152,7 @@ "bridging.estimatedTime.minutesAndSeconds": "~{{minutes}} mnt {{seconds}} dtk", "bridging.estimatedTime.minutesOnly": "~{{minutes}} mnt", "bridging.estimatedTime.secondsOnly": "~{{seconds}} dtk", + "bridgingPopularTokens.banner.description": "DOGE, XRP, XLP — now available on Unichain.", "chart.candlestick": "Kandil", "chart.error.pools": "Tidak dapat menampilkan data historis untuk pool saat ini.", "chart.error.tokens": "Tidak dapat menampilkan data historis untuk token saat ini.", @@ -1437,17 +1438,32 @@ "poolFinder.availablePools.notFound.description": "Cadangan aset v2 yang sesuai tidak ditemukan. Periksa kembali pilihan tokenmu dan pastikan kamu telah terhubung dengan dompet yang benar.", "pools.explore": "Jelajahi pool", "portfolio.activity.filters.timePeriod.all": "Sepanjang periode", + "portfolio.activity.filters.transactionType.addLiquidity": "Add Liquidity", "portfolio.activity.filters.transactionType.all": "Semua jenis", - "portfolio.activity.filters.transactionType.deposits": "Setoran", - "portfolio.activity.filters.transactionType.staking": "Staking", + "portfolio.activity.filters.transactionType.approvals": "Approvals", + "portfolio.activity.filters.transactionType.claimFees": "Claim Fees", + "portfolio.activity.filters.transactionType.createPool": "Create Pool", + "portfolio.activity.filters.transactionType.mints": "Mints", + "portfolio.activity.filters.transactionType.removeLiquidity": "Remove Liquidity", "portfolio.activity.filters.transactionType.swaps": "Pertukaran", + "portfolio.activity.filters.transactionType.wraps": "Wraps", + "portfolio.activity.table.column.address": "Address", + "portfolio.activity.table.column.amount": "Amount", + "portfolio.activity.table.column.time": "Time", + "portfolio.activity.table.column.type": "Type", "portfolio.activity.title": "Aktivitas", - "portfolio.connectWallet.summary": "Track tokens, pools, and more across {{amount}}+ networks", "portfolio.defi.title": "DeFi", "portfolio.description": "Melacak portofolio kripto di semua chain dan protokol", + "portfolio.disconnected.connectWallet.cta": "Track your portfolio", + "portfolio.disconnected.cta.description": "Track tokens, positions, NFTs and more across {{numNetworks}}+ networks", + "portfolio.disconnected.demoWallet.description": "This is a view-only example. Connect your wallet to manage your portfolio.", + "portfolio.disconnected.demoWallet.title": "Demo wallet", + "portfolio.disconnected.viewYourPortfolio.cta": "to view your portfolio", "portfolio.nfts.title": "NFT", "portfolio.overview.title": "Ikhtisar", "portfolio.title": "Portofolio", + "portfolio.tokens.balance.totalTokens": "{{numTokens}} tokens", + "portfolio.tokens.emptyState": "No tokens", "portfolio.tokens.table.column.allocation": "Alokasi", "portfolio.tokens.table.column.balance": "Saldo", "portfolio.tokens.table.column.change1d": "Perubahan 1 Hari", @@ -2505,6 +2521,12 @@ "uwulink.error.insufficientTokens": "Tidak cukup {{tokenSymbol}} di {{chain}}", "v2.notAvailable": "Uniswap V2 tidak tersedia di jaringan ini.", "wallet.appSignIn": "Masuk dengan aplikasi", + "wallet.connecting.description": "Complete connection in your wallet", + "wallet.connecting.solanaPrompt": "Use Solana on Uniswap", + "wallet.connecting.solanaPrompt.button": "Use Solana", + "wallet.connecting.solanaPrompt.description": "You’ll be asked to connect again in MetaMask", + "wallet.connecting.title.evm": "Connect to {{walletName}}", + "wallet.connecting.title.svm": "Connect to {{walletName}} on Solana", "wallet.connectingAgreement": "Dengan menghubungkan dompet, kamu menyetujui Ketentuan Layanan Uniswap Labs dan menyetujui Kebijakan Privasi.", "wallet.connectionFailed.message": "Upaya koneksi gagal. Silakan coba lagi dan ikuti langkah-langkah untuk menghubungkan di dompetmu.", "wallet.mismatch.popup.description": "Dompet terhubungmu tidak mendukung beberapa fitur.", diff --git a/packages/uniswap/src/i18n/locales/translations/it-IT.json b/packages/uniswap/src/i18n/locales/translations/it-IT.json index fe13f5ece22..275f8c80b31 100644 --- a/packages/uniswap/src/i18n/locales/translations/it-IT.json +++ b/packages/uniswap/src/i18n/locales/translations/it-IT.json @@ -1675,6 +1675,7 @@ "settings.action.lock": "Blocca il portafoglio", "settings.action.privacy": "Politica sulla riservatezza", "settings.action.terms": "Termini di servizio", + "settings.connectWalletPlatform.warning": "Per utilizzare Uniswap su {{platform}}, connettiti a un portafoglio che supporti {{platform}}.", "settings.footer": "Fatto con amore, \nUniswap Team 🦄", "settings.hideSmallBalances": "Nascondi piccoli saldi", "settings.hideSmallBalances.subtitle": "I saldi inferiori a 1 USD saranno nascosti dal tuo portafoglio.", @@ -1845,7 +1846,7 @@ "smartWallets.postSwapNudge.title.dapp": "Questa app supporta i portafogli intelligenti", "smartWallets.unavailableModal.description": "Un altro fornitore di wallet gestisce ora le impostazioni del wallet intelligente per {{displayName}}. Puoi continuare a utilizzare Uniswap normalmente.", "smartWallets.unavailableModal.title": "Funzionalità del portafoglio intelligente non disponibili", - "solanaPromo.banner.description": "Scambia i token Solana direttamente sulla Web App Uniswap.", + "solanaPromo.banner.description": "Scambia i token Solana direttamente su Uniswap.", "solanaPromo.banner.title": "Solana è ora disponibile", "solanaPromo.modal.connectWallet": "Collega il tuo portafoglio Solana preferito", "solanaPromo.modal.startSwapping.button": "Inizia a scambiare su Solana", diff --git a/packages/uniswap/src/i18n/locales/translations/ja-JP.json b/packages/uniswap/src/i18n/locales/translations/ja-JP.json index 3d432bde27d..2add2c812a9 100644 --- a/packages/uniswap/src/i18n/locales/translations/ja-JP.json +++ b/packages/uniswap/src/i18n/locales/translations/ja-JP.json @@ -152,6 +152,7 @@ "bridging.estimatedTime.minutesAndSeconds": "約 {{minutes}} 分 {{seconds}} 秒", "bridging.estimatedTime.minutesOnly": "約 {{minutes}} 分", "bridging.estimatedTime.secondsOnly": "約 {{seconds}} 秒", + "bridgingPopularTokens.banner.description": "DOGE, XRP, XLP — now available on Unichain.", "chart.candlestick": "ローソク足", "chart.error.pools": "現在のプールの履歴データを表示できません。", "chart.error.tokens": "現在のトークンの履歴データを表示できません。", @@ -1437,17 +1438,32 @@ "poolFinder.availablePools.notFound.description": "一致する v2 プールが見つかりませんでした。選択したトークンを再確認し、正しいウォレットに接続されていることを確認してください。", "pools.explore": "プールを探索", "portfolio.activity.filters.timePeriod.all": "全期間", + "portfolio.activity.filters.transactionType.addLiquidity": "Add Liquidity", "portfolio.activity.filters.transactionType.all": "全種類", - "portfolio.activity.filters.transactionType.deposits": "預け入れ", - "portfolio.activity.filters.transactionType.staking": "ステーキング", + "portfolio.activity.filters.transactionType.approvals": "Approvals", + "portfolio.activity.filters.transactionType.claimFees": "Claim Fees", + "portfolio.activity.filters.transactionType.createPool": "Create Pool", + "portfolio.activity.filters.transactionType.mints": "Mints", + "portfolio.activity.filters.transactionType.removeLiquidity": "Remove Liquidity", "portfolio.activity.filters.transactionType.swaps": "スワップ", + "portfolio.activity.filters.transactionType.wraps": "Wraps", + "portfolio.activity.table.column.address": "Address", + "portfolio.activity.table.column.amount": "Amount", + "portfolio.activity.table.column.time": "Time", + "portfolio.activity.table.column.type": "Type", "portfolio.activity.title": "アクティビティ", - "portfolio.connectWallet.summary": "Track tokens, pools, and more across {{amount}}+ networks", "portfolio.defi.title": "DeFi", "portfolio.description": "チェーンとプロトコルすべてにわたって暗号資産ポートフォリオを追跡します", + "portfolio.disconnected.connectWallet.cta": "Track your portfolio", + "portfolio.disconnected.cta.description": "Track tokens, positions, NFTs and more across {{numNetworks}}+ networks", + "portfolio.disconnected.demoWallet.description": "This is a view-only example. Connect your wallet to manage your portfolio.", + "portfolio.disconnected.demoWallet.title": "Demo wallet", + "portfolio.disconnected.viewYourPortfolio.cta": "to view your portfolio", "portfolio.nfts.title": "NFT", "portfolio.overview.title": "概要", "portfolio.title": "ポートフォリオ", + "portfolio.tokens.balance.totalTokens": "{{numTokens}} tokens", + "portfolio.tokens.emptyState": "No tokens", "portfolio.tokens.table.column.allocation": "割り当て", "portfolio.tokens.table.column.balance": "残高", "portfolio.tokens.table.column.change1d": "1 日の変更", @@ -2505,6 +2521,12 @@ "uwulink.error.insufficientTokens": "{{chain}} の {{tokenSymbol}} が十分ではありません", "v2.notAvailable": "Uniswap V2 はこのネットワークでは利用できません。", "wallet.appSignIn": "アプリでログイン", + "wallet.connecting.description": "Complete connection in your wallet", + "wallet.connecting.solanaPrompt": "Use Solana on Uniswap", + "wallet.connecting.solanaPrompt.button": "Use Solana", + "wallet.connecting.solanaPrompt.description": "You’ll be asked to connect again in MetaMask", + "wallet.connecting.title.evm": "Connect to {{walletName}}", + "wallet.connecting.title.svm": "Connect to {{walletName}} on Solana", "wallet.connectingAgreement": "ウォレットを接続すると、Uniswap Labs の利用規約に同意し、プライバシー ポリシーに同意したことになります。", "wallet.connectionFailed.message": "Connection attempt failed. Please try again, following the steps to connect in your wallet.", "wallet.mismatch.popup.description": "接続中のウォレットでは一部の機能がサポートされていません。", diff --git a/packages/uniswap/src/i18n/locales/translations/ko-KR.json b/packages/uniswap/src/i18n/locales/translations/ko-KR.json index 92215e92477..8e7df5d1fcf 100644 --- a/packages/uniswap/src/i18n/locales/translations/ko-KR.json +++ b/packages/uniswap/src/i18n/locales/translations/ko-KR.json @@ -152,6 +152,7 @@ "bridging.estimatedTime.minutesAndSeconds": "~{{minutes}}분 {{seconds}}초", "bridging.estimatedTime.minutesOnly": "~{{minutes}}분", "bridging.estimatedTime.secondsOnly": "~{{seconds}}초", + "bridgingPopularTokens.banner.description": "DOGE, XRP, XLP — now available on Unichain.", "chart.candlestick": "촛대", "chart.error.pools": "현재 풀에 대한 기록 데이터를 표시할 수 없습니다.", "chart.error.tokens": "현재 토큰에 대한 기록 데이터를 표시할 수 없습니다.", @@ -1437,17 +1438,32 @@ "poolFinder.availablePools.notFound.description": "일치하는 v2 풀을 찾지 못했습니다. 토큰 선택을 다시 한번 확인하고 올바른 지갑에 연결되어 있는지 확인하세요.", "pools.explore": "풀 탐색", "portfolio.activity.filters.timePeriod.all": "누적", + "portfolio.activity.filters.transactionType.addLiquidity": "Add Liquidity", "portfolio.activity.filters.transactionType.all": "모든 유형", - "portfolio.activity.filters.transactionType.deposits": "입금", - "portfolio.activity.filters.transactionType.staking": "스테이킹", + "portfolio.activity.filters.transactionType.approvals": "Approvals", + "portfolio.activity.filters.transactionType.claimFees": "Claim Fees", + "portfolio.activity.filters.transactionType.createPool": "Create Pool", + "portfolio.activity.filters.transactionType.mints": "Mints", + "portfolio.activity.filters.transactionType.removeLiquidity": "Remove Liquidity", "portfolio.activity.filters.transactionType.swaps": "스왑", + "portfolio.activity.filters.transactionType.wraps": "Wraps", + "portfolio.activity.table.column.address": "Address", + "portfolio.activity.table.column.amount": "Amount", + "portfolio.activity.table.column.time": "Time", + "portfolio.activity.table.column.type": "Type", "portfolio.activity.title": "활동", - "portfolio.connectWallet.summary": "Track tokens, pools, and more across {{amount}}+ networks", "portfolio.defi.title": "DeFi", "portfolio.description": "모든 체인과 프로토콜을 아우르는 암호화폐 포트폴리오 추적", + "portfolio.disconnected.connectWallet.cta": "Track your portfolio", + "portfolio.disconnected.cta.description": "Track tokens, positions, NFTs and more across {{numNetworks}}+ networks", + "portfolio.disconnected.demoWallet.description": "This is a view-only example. Connect your wallet to manage your portfolio.", + "portfolio.disconnected.demoWallet.title": "Demo wallet", + "portfolio.disconnected.viewYourPortfolio.cta": "to view your portfolio", "portfolio.nfts.title": "NFT", "portfolio.overview.title": "개요", "portfolio.title": "포트폴리오", + "portfolio.tokens.balance.totalTokens": "{{numTokens}} tokens", + "portfolio.tokens.emptyState": "No tokens", "portfolio.tokens.table.column.allocation": "할당", "portfolio.tokens.table.column.balance": "잔액", "portfolio.tokens.table.column.change1d": "1일 변동", @@ -2505,6 +2521,12 @@ "uwulink.error.insufficientTokens": "{{chain}}에 {{tokenSymbol}}이 충분하지 않습니다.", "v2.notAvailable": "이 네트워크에서는 Uniswap V2를 사용할 수 없습니다.", "wallet.appSignIn": "앱으로 로그인", + "wallet.connecting.description": "Complete connection in your wallet", + "wallet.connecting.solanaPrompt": "Use Solana on Uniswap", + "wallet.connecting.solanaPrompt.button": "Use Solana", + "wallet.connecting.solanaPrompt.description": "You’ll be asked to connect again in MetaMask", + "wallet.connecting.title.evm": "Connect to {{walletName}}", + "wallet.connecting.title.svm": "Connect to {{walletName}} on Solana", "wallet.connectingAgreement": "지갑을 연결하면 Uniswap Labs의 서비스 약관개인정보 보호정책에 동의하는 것으로 간주됩니다.", "wallet.connectionFailed.message": "Connection attempt failed. Please try again, following the steps to connect in your wallet.", "wallet.mismatch.popup.description": "연결된 지갑에서 일부 기능을 지원하지 않습니다.", diff --git a/packages/uniswap/src/i18n/locales/translations/ms-MY.json b/packages/uniswap/src/i18n/locales/translations/ms-MY.json index f2a08e7fe70..dd31542a088 100644 --- a/packages/uniswap/src/i18n/locales/translations/ms-MY.json +++ b/packages/uniswap/src/i18n/locales/translations/ms-MY.json @@ -1675,6 +1675,7 @@ "settings.action.lock": "Kunci dompet", "settings.action.privacy": "Dasar privasi", "settings.action.terms": "Syarat perkhidmatan", + "settings.connectWalletPlatform.warning": "Untuk menggunakan Uniswap pada {{platform}}, sambung ke dompet yang menyokong {{platform}}.", "settings.footer": "Dibuat dengan penuh kasih sayang, \nPasukan Uniswap 🦄", "settings.hideSmallBalances": "Sembunyikan baki kecil", "settings.hideSmallBalances.subtitle": "Baki di bawah 1 USD akan disembunyikan daripada portfolio anda.", @@ -1845,7 +1846,7 @@ "smartWallets.postSwapNudge.title.dapp": "Aplikasi ini menyokong dompet pintar", "smartWallets.unavailableModal.description": "Pembekal dompet yang berbeza kini menguruskan tetapan dompet pintar untuk {{displayName}}. Anda boleh terus menggunakan Uniswap seperti biasa.", "smartWallets.unavailableModal.title": "Ciri dompet pintar tidak tersedia", - "solanaPromo.banner.description": "Berdagang token Solana terus pada Apl Web Uniswap.", + "solanaPromo.banner.description": "Berdagang token Solana terus pada Uniswap.", "solanaPromo.banner.title": "Solana kini tersedia", "solanaPromo.modal.connectWallet": "Sambungkan dompet Solana kegemaran anda", "solanaPromo.modal.startSwapping.button": "Mula bertukar pada Solana", diff --git a/packages/uniswap/src/i18n/locales/translations/nl-NL.json b/packages/uniswap/src/i18n/locales/translations/nl-NL.json index 8b8d74c3d7b..81f0d03cc5d 100644 --- a/packages/uniswap/src/i18n/locales/translations/nl-NL.json +++ b/packages/uniswap/src/i18n/locales/translations/nl-NL.json @@ -152,6 +152,7 @@ "bridging.estimatedTime.minutesAndSeconds": "~{{minutes}}min {{seconds}}s", "bridging.estimatedTime.minutesOnly": "~{{minutes}}min", "bridging.estimatedTime.secondsOnly": "~{{seconds}}s", + "bridgingPopularTokens.banner.description": "DOGE, XRP, XLP — now available on Unichain.", "chart.candlestick": "Candlestick", "chart.error.pools": "Kon de historische data voor de huidige pool niet weergeven.", "chart.error.tokens": "Kon de historische data voor het huidige token niet weergeven.", @@ -1437,17 +1438,32 @@ "poolFinder.availablePools.notFound.description": "Geen overeenkomende v2-pools gevonden. Controleer je tokenselectie nogmaals en zorg ervoor dat je verbonden bent met de juiste wallet.", "pools.explore": "Pools verkennen", "portfolio.activity.filters.timePeriod.all": "Altijd", + "portfolio.activity.filters.transactionType.addLiquidity": "Add Liquidity", "portfolio.activity.filters.transactionType.all": "Alle typen", - "portfolio.activity.filters.transactionType.deposits": "Stortingen", - "portfolio.activity.filters.transactionType.staking": "Staking", + "portfolio.activity.filters.transactionType.approvals": "Approvals", + "portfolio.activity.filters.transactionType.claimFees": "Claim Fees", + "portfolio.activity.filters.transactionType.createPool": "Create Pool", + "portfolio.activity.filters.transactionType.mints": "Mints", + "portfolio.activity.filters.transactionType.removeLiquidity": "Remove Liquidity", "portfolio.activity.filters.transactionType.swaps": "Swaps", + "portfolio.activity.filters.transactionType.wraps": "Wraps", + "portfolio.activity.table.column.address": "Address", + "portfolio.activity.table.column.amount": "Amount", + "portfolio.activity.table.column.time": "Time", + "portfolio.activity.table.column.type": "Type", "portfolio.activity.title": "Activiteit", - "portfolio.connectWallet.summary": "Track tokens, pools, and more across {{amount}}+ networks", "portfolio.defi.title": "DeFi", "portfolio.description": "Volg je cryptoportefeuille in alle chains en protocollen", + "portfolio.disconnected.connectWallet.cta": "Track your portfolio", + "portfolio.disconnected.cta.description": "Track tokens, positions, NFTs and more across {{numNetworks}}+ networks", + "portfolio.disconnected.demoWallet.description": "This is a view-only example. Connect your wallet to manage your portfolio.", + "portfolio.disconnected.demoWallet.title": "Demo wallet", + "portfolio.disconnected.viewYourPortfolio.cta": "to view your portfolio", "portfolio.nfts.title": "NFT's", "portfolio.overview.title": "Overzicht", "portfolio.title": "Portefeuille", + "portfolio.tokens.balance.totalTokens": "{{numTokens}} tokens", + "portfolio.tokens.emptyState": "No tokens", "portfolio.tokens.table.column.allocation": "Toewijzing", "portfolio.tokens.table.column.balance": "Saldo", "portfolio.tokens.table.column.change1d": "1d-wijziging", @@ -2505,6 +2521,12 @@ "uwulink.error.insufficientTokens": "Onvoldoende {{tokenSymbol}} op {{chain}}", "v2.notAvailable": "Uniswap V2 is niet beschikbaar op dit netwerk.", "wallet.appSignIn": "Aanmelden met de app", + "wallet.connecting.description": "Complete connection in your wallet", + "wallet.connecting.solanaPrompt": "Use Solana on Uniswap", + "wallet.connecting.solanaPrompt.button": "Use Solana", + "wallet.connecting.solanaPrompt.description": "You’ll be asked to connect again in MetaMask", + "wallet.connecting.title.evm": "Connect to {{walletName}}", + "wallet.connecting.title.svm": "Connect to {{walletName}} on Solana", "wallet.connectingAgreement": "Door een wallet te verbinden, ga je akkoord met de Servicevoorwaarden van Uniswap Labs en geef je toestemming voor het Privacybeleid.", "wallet.connectionFailed.message": "Verbindingspoging mislukt. Probeer het opnieuw en volg de stappen om verbinding te maken in je wallet.", "wallet.mismatch.popup.description": "Sommige functies worden niet ondersteund door je verbonden wallet.", diff --git a/packages/uniswap/src/i18n/locales/translations/pl-PL.json b/packages/uniswap/src/i18n/locales/translations/pl-PL.json index 2d44ff150af..29ec82c2163 100644 --- a/packages/uniswap/src/i18n/locales/translations/pl-PL.json +++ b/packages/uniswap/src/i18n/locales/translations/pl-PL.json @@ -1675,6 +1675,7 @@ "settings.action.lock": "Zablokuj portfel", "settings.action.privacy": "Polityka prywatności", "settings.action.terms": "Warunki usługi", + "settings.connectWalletPlatform.warning": "Aby użyć Uniswap na {{platform}}, połącz się z portfelem, który obsługuje {{platform}}.", "settings.footer": "Wykonane z miłością, \nZespół Uniswap 🦄", "settings.hideSmallBalances": "Ukryj małe salda", "settings.hideSmallBalances.subtitle": "Salda poniżej 1 USD nie będą widoczne w Twoim portfelu.", @@ -1845,7 +1846,7 @@ "smartWallets.postSwapNudge.title.dapp": "Ta aplikacja obsługuje inteligentne portfele", "smartWallets.unavailableModal.description": "Inny dostawca portfela zarządza teraz ustawieniami inteligentnego portfela dla {{displayName}}. Możesz nadal używać Uniswap jak zwykle.", "smartWallets.unavailableModal.title": "Funkcje inteligentnego portfela są niedostępne", - "solanaPromo.banner.description": "Handluj tokenami Solana bezpośrednio w aplikacji internetowej Uniswap.", + "solanaPromo.banner.description": "Handluj tokenami Solana bezpośrednio na platformie Uniswap.", "solanaPromo.banner.title": "Solana jest już dostępna", "solanaPromo.modal.connectWallet": "Podłącz swój ulubiony portfel Solana", "solanaPromo.modal.startSwapping.button": "Rozpocznij wymianę na Solanie", diff --git a/packages/uniswap/src/i18n/locales/translations/pt-PT.json b/packages/uniswap/src/i18n/locales/translations/pt-PT.json index fb1b0588fb6..cb8e48d7a01 100644 --- a/packages/uniswap/src/i18n/locales/translations/pt-PT.json +++ b/packages/uniswap/src/i18n/locales/translations/pt-PT.json @@ -152,6 +152,7 @@ "bridging.estimatedTime.minutesAndSeconds": "~{{minutes}} min {{seconds}} s", "bridging.estimatedTime.minutesOnly": "~{{minutes}} min", "bridging.estimatedTime.secondsOnly": "~{{seconds}} s", + "bridgingPopularTokens.banner.description": "DOGE, XRP, XLP — now available on Unichain.", "chart.candlestick": "Vela", "chart.error.pools": "Não foi possível exibir dados históricos do pool atual.", "chart.error.tokens": "Não foi possível exibir dados históricos do token atual.", @@ -1437,17 +1438,32 @@ "poolFinder.availablePools.notFound.description": "Não foram encontrados pools v2 correspondentes. Verifique novamente sua seleção de tokens e se está acessando a carteira certa.", "pools.explore": "Explorar pools", "portfolio.activity.filters.timePeriod.all": "Todo o período", + "portfolio.activity.filters.transactionType.addLiquidity": "Add Liquidity", "portfolio.activity.filters.transactionType.all": "Todos os tipos", - "portfolio.activity.filters.transactionType.deposits": "Depósitos", - "portfolio.activity.filters.transactionType.staking": "Staking", + "portfolio.activity.filters.transactionType.approvals": "Approvals", + "portfolio.activity.filters.transactionType.claimFees": "Claim Fees", + "portfolio.activity.filters.transactionType.createPool": "Create Pool", + "portfolio.activity.filters.transactionType.mints": "Mints", + "portfolio.activity.filters.transactionType.removeLiquidity": "Remove Liquidity", "portfolio.activity.filters.transactionType.swaps": "Swaps", + "portfolio.activity.filters.transactionType.wraps": "Wraps", + "portfolio.activity.table.column.address": "Address", + "portfolio.activity.table.column.amount": "Amount", + "portfolio.activity.table.column.time": "Time", + "portfolio.activity.table.column.type": "Type", "portfolio.activity.title": "Atividade", - "portfolio.connectWallet.summary": "Track tokens, pools, and more across {{amount}}+ networks", "portfolio.defi.title": "DeFi", "portfolio.description": "Acompanhe seu portfólio de criptos em todas as redes e protocolos", + "portfolio.disconnected.connectWallet.cta": "Track your portfolio", + "portfolio.disconnected.cta.description": "Track tokens, positions, NFTs and more across {{numNetworks}}+ networks", + "portfolio.disconnected.demoWallet.description": "This is a view-only example. Connect your wallet to manage your portfolio.", + "portfolio.disconnected.demoWallet.title": "Demo wallet", + "portfolio.disconnected.viewYourPortfolio.cta": "to view your portfolio", "portfolio.nfts.title": "NFTs", "portfolio.overview.title": "Visão geral", "portfolio.title": "Portfólio", + "portfolio.tokens.balance.totalTokens": "{{numTokens}} tokens", + "portfolio.tokens.emptyState": "No tokens", "portfolio.tokens.table.column.allocation": "Alocação", "portfolio.tokens.table.column.balance": "Saldo", "portfolio.tokens.table.column.change1d": "Alteração de 1 dia", @@ -2505,6 +2521,12 @@ "uwulink.error.insufficientTokens": "Não há {{tokenSymbol}} suficiente em {{chain}}", "v2.notAvailable": "A Uniswap V2 não está disponível nesta rede.", "wallet.appSignIn": "Entrar com o aplicativo", + "wallet.connecting.description": "Complete connection in your wallet", + "wallet.connecting.solanaPrompt": "Use Solana on Uniswap", + "wallet.connecting.solanaPrompt.button": "Use Solana", + "wallet.connecting.solanaPrompt.description": "You’ll be asked to connect again in MetaMask", + "wallet.connecting.title.evm": "Connect to {{walletName}}", + "wallet.connecting.title.svm": "Connect to {{walletName}} on Solana", "wallet.connectingAgreement": "Ao conectar uma carteira, você concorda com os Termos de serviço e a Política de privacidade da Uniswap Labs.", "wallet.connectionFailed.message": "Connection attempt failed. Please try again, following the steps to connect in your wallet.", "wallet.mismatch.popup.description": "Sua carteira não é compatível com alguns recursos.", diff --git a/packages/uniswap/src/i18n/locales/translations/ru-RU.json b/packages/uniswap/src/i18n/locales/translations/ru-RU.json index a4e2f3b7bf7..0e9fe2c606c 100644 --- a/packages/uniswap/src/i18n/locales/translations/ru-RU.json +++ b/packages/uniswap/src/i18n/locales/translations/ru-RU.json @@ -152,6 +152,7 @@ "bridging.estimatedTime.minutesAndSeconds": "прибл. {{minutes}} мин. {{seconds}} с.", "bridging.estimatedTime.minutesOnly": "прибл. {{minutes}} мин.", "bridging.estimatedTime.secondsOnly": "прибл. {{seconds}} с.", + "bridgingPopularTokens.banner.description": "DOGE, XRP, XLP — now available on Unichain.", "chart.candlestick": "Свечной график", "chart.error.pools": "Невозможно отобразить исторические данные для текущего пула.", "chart.error.tokens": "Невозможно отобразить исторические данные для текущего токена.", @@ -1437,17 +1438,32 @@ "poolFinder.availablePools.notFound.description": "Соответствующие пулы v2 не найдены. Еще раз проверьте выбранные токены и убедитесь, что подключились к правильному кошельку.", "pools.explore": "Исследование пулов", "portfolio.activity.filters.timePeriod.all": "Все время", + "portfolio.activity.filters.transactionType.addLiquidity": "Add Liquidity", "portfolio.activity.filters.transactionType.all": "Все типы", - "portfolio.activity.filters.transactionType.deposits": "Депозиты", - "portfolio.activity.filters.transactionType.staking": "Стейкинг", + "portfolio.activity.filters.transactionType.approvals": "Approvals", + "portfolio.activity.filters.transactionType.claimFees": "Claim Fees", + "portfolio.activity.filters.transactionType.createPool": "Create Pool", + "portfolio.activity.filters.transactionType.mints": "Mints", + "portfolio.activity.filters.transactionType.removeLiquidity": "Remove Liquidity", "portfolio.activity.filters.transactionType.swaps": "Свопы", + "portfolio.activity.filters.transactionType.wraps": "Wraps", + "portfolio.activity.table.column.address": "Address", + "portfolio.activity.table.column.amount": "Amount", + "portfolio.activity.table.column.time": "Time", + "portfolio.activity.table.column.type": "Type", "portfolio.activity.title": "Активность", - "portfolio.connectWallet.summary": "Track tokens, pools, and more across {{amount}}+ networks", "portfolio.defi.title": "DeFi", "portfolio.description": "Отслеживайте свои криптовалюты во всех блокчейнах и протоколах", + "portfolio.disconnected.connectWallet.cta": "Track your portfolio", + "portfolio.disconnected.cta.description": "Track tokens, positions, NFTs and more across {{numNetworks}}+ networks", + "portfolio.disconnected.demoWallet.description": "This is a view-only example. Connect your wallet to manage your portfolio.", + "portfolio.disconnected.demoWallet.title": "Demo wallet", + "portfolio.disconnected.viewYourPortfolio.cta": "to view your portfolio", "portfolio.nfts.title": "NFT", "portfolio.overview.title": "Обзор", "portfolio.title": "Портфель", + "portfolio.tokens.balance.totalTokens": "{{numTokens}} tokens", + "portfolio.tokens.emptyState": "No tokens", "portfolio.tokens.table.column.allocation": "Распределение", "portfolio.tokens.table.column.balance": "Баланс", "portfolio.tokens.table.column.change1d": "Изменения за 1 дн.", @@ -2505,6 +2521,12 @@ "uwulink.error.insufficientTokens": "Недостаточно {{tokenSymbol}} в {{chain}}", "v2.notAvailable": "Протокол Uniswap V2 недоступен в этой сети.", "wallet.appSignIn": "Войти через приложение", + "wallet.connecting.description": "Complete connection in your wallet", + "wallet.connecting.solanaPrompt": "Use Solana on Uniswap", + "wallet.connecting.solanaPrompt.button": "Use Solana", + "wallet.connecting.solanaPrompt.description": "You’ll be asked to connect again in MetaMask", + "wallet.connecting.title.evm": "Connect to {{walletName}}", + "wallet.connecting.title.svm": "Connect to {{walletName}} on Solana", "wallet.connectingAgreement": "Подключая кошелек, вы принимаете Условия обслуживания и Политику конфиденциальности Uniswap Labs.", "wallet.connectionFailed.message": "Connection attempt failed. Please try again, following the steps to connect in your wallet.", "wallet.mismatch.popup.description": "В подключенном кошельке не поддерживаются некоторые функции.", diff --git a/packages/uniswap/src/i18n/locales/translations/sl-SI.json b/packages/uniswap/src/i18n/locales/translations/sl-SI.json index 7953e1a00d8..223da00c4b1 100644 --- a/packages/uniswap/src/i18n/locales/translations/sl-SI.json +++ b/packages/uniswap/src/i18n/locales/translations/sl-SI.json @@ -1675,6 +1675,7 @@ "settings.action.lock": "Zakleni denarnico", "settings.action.privacy": "Politika zasebnosti", "settings.action.terms": "Pogoji storitve", + "settings.connectWalletPlatform.warning": "Za uporabo Uniswapa na {{platform}}se povežite z denarnico, ki podpira {{platform}}.", "settings.footer": "Narejeno z ljubeznijo, \nekipa Uniswap 🦄", "settings.hideSmallBalances": "Skrij majhna stanja", "settings.hideSmallBalances.subtitle": "Stanja pod 1 USD bodo skrita v vašem portfelju.", @@ -1845,7 +1846,7 @@ "smartWallets.postSwapNudge.title.dapp": "Ta aplikacija podpira pametne denarnice", "smartWallets.unavailableModal.description": "Nastavitve pametne denarnice za {{displayName}}zdaj upravlja drug ponudnik denarnic. Uniswap lahko še naprej uporabljate kot običajno.", "smartWallets.unavailableModal.title": "Funkcije pametne denarnice niso na voljo", - "solanaPromo.banner.description": "Trgujte z žetoni Solana neposredno v spletni aplikaciji Uniswap.", + "solanaPromo.banner.description": "Trgujte z žetoni Solana neposredno na Uniswapu.", "solanaPromo.banner.title": "Solana je zdaj na voljo", "solanaPromo.modal.connectWallet": "Povežite svojo najljubšo denarnico Solana", "solanaPromo.modal.startSwapping.button": "Začnite menjati na Solani", diff --git a/packages/uniswap/src/i18n/locales/translations/sr-SP.json b/packages/uniswap/src/i18n/locales/translations/sr-SP.json index a65b21f9ec0..afaef20798b 100644 --- a/packages/uniswap/src/i18n/locales/translations/sr-SP.json +++ b/packages/uniswap/src/i18n/locales/translations/sr-SP.json @@ -1675,6 +1675,7 @@ "settings.action.lock": "Закључајте новчаник", "settings.action.privacy": "Правила о приватности", "settings.action.terms": "Услови коришћења", + "settings.connectWalletPlatform.warning": "To use Uniswap on {{platform}}, connect to a wallet that supports {{platform}}.", "settings.footer": "Направљен с љубављу, \nУнисвап тим 🦄", "settings.hideSmallBalances": "Сакријте мале биланце", "settings.hideSmallBalances.subtitle": "Balances under 1 USD will be hidden from your portfolio.", @@ -1845,7 +1846,7 @@ "smartWallets.postSwapNudge.title.dapp": "This app supports smart wallets", "smartWallets.unavailableModal.description": "A different wallet provider is now managing smart wallet settings for {{displayName}}. You can continue using Uniswap as normal.", "smartWallets.unavailableModal.title": "Smart wallet features unavailable", - "solanaPromo.banner.description": "Trade Solana tokens directly on the Uniswap Web App.", + "solanaPromo.banner.description": "Trade Solana tokens directly on Uniswap.", "solanaPromo.banner.title": "Solana is now available", "solanaPromo.modal.connectWallet": "Connect your favorite Solana wallet", "solanaPromo.modal.startSwapping.button": "Start swapping on Solana", diff --git a/packages/uniswap/src/i18n/locales/translations/sv-SE.json b/packages/uniswap/src/i18n/locales/translations/sv-SE.json index 87294b31017..dd01404d9fc 100644 --- a/packages/uniswap/src/i18n/locales/translations/sv-SE.json +++ b/packages/uniswap/src/i18n/locales/translations/sv-SE.json @@ -1675,6 +1675,7 @@ "settings.action.lock": "Lås plånbok", "settings.action.privacy": "Integritetspolicy", "settings.action.terms": "Användarvillkor", + "settings.connectWalletPlatform.warning": "För att använda Uniswap på {{platform}}, anslut till en plånbok som stöder {{platform}}.", "settings.footer": "Tillverkad med kärlek, \nUniswap Team 🦄", "settings.hideSmallBalances": "Dölj små saldon", "settings.hideSmallBalances.subtitle": "Saldon under 1 USD kommer att döljas från din portfölj.", @@ -1845,7 +1846,7 @@ "smartWallets.postSwapNudge.title.dapp": "Den här appen stöder smarta plånböcker", "smartWallets.unavailableModal.description": "En annan plånboksleverantör hanterar nu smarta plånboksinställningar för {{displayName}}. Du kan fortsätta använda Uniswap som vanligt.", "smartWallets.unavailableModal.title": "Smarta plånboksfunktioner är inte tillgängliga", - "solanaPromo.banner.description": "Handla Solana-tokens direkt i Uniswap-webbappen.", + "solanaPromo.banner.description": "Handla Solana-tokens direkt på Uniswap.", "solanaPromo.banner.title": "Solana är nu tillgänglig", "solanaPromo.modal.connectWallet": "Anslut din favorit Solana-plånbok", "solanaPromo.modal.startSwapping.button": "Börja byta på Solana", diff --git a/packages/uniswap/src/i18n/locales/translations/sw-TZ.json b/packages/uniswap/src/i18n/locales/translations/sw-TZ.json index 8015c9a4637..7e2958541b6 100644 --- a/packages/uniswap/src/i18n/locales/translations/sw-TZ.json +++ b/packages/uniswap/src/i18n/locales/translations/sw-TZ.json @@ -1675,6 +1675,7 @@ "settings.action.lock": "Funga mkoba", "settings.action.privacy": "Sera ya faragha", "settings.action.terms": "Masharti ya huduma", + "settings.connectWalletPlatform.warning": "To use Uniswap on {{platform}}, connect to a wallet that supports {{platform}}.", "settings.footer": "Imetengenezwa kwa upendo, \nTimu ya Uniswap 🦄", "settings.hideSmallBalances": "Ficha mizani ndogo", "settings.hideSmallBalances.subtitle": "Balances under 1 USD will be hidden from your portfolio.", @@ -1845,7 +1846,7 @@ "smartWallets.postSwapNudge.title.dapp": "This app supports smart wallets", "smartWallets.unavailableModal.description": "A different wallet provider is now managing smart wallet settings for {{displayName}}. You can continue using Uniswap as normal.", "smartWallets.unavailableModal.title": "Smart wallet features unavailable", - "solanaPromo.banner.description": "Trade Solana tokens directly on the Uniswap Web App.", + "solanaPromo.banner.description": "Trade Solana tokens directly on Uniswap.", "solanaPromo.banner.title": "Solana is now available", "solanaPromo.modal.connectWallet": "Connect your favorite Solana wallet", "solanaPromo.modal.startSwapping.button": "Start swapping on Solana", diff --git a/packages/uniswap/src/i18n/locales/translations/tr-TR.json b/packages/uniswap/src/i18n/locales/translations/tr-TR.json index 5b508028083..8282c8779ad 100644 --- a/packages/uniswap/src/i18n/locales/translations/tr-TR.json +++ b/packages/uniswap/src/i18n/locales/translations/tr-TR.json @@ -152,6 +152,7 @@ "bridging.estimatedTime.minutesAndSeconds": "~{{minutes}} dk {{seconds}} sn", "bridging.estimatedTime.minutesOnly": "~{{minutes}} dk", "bridging.estimatedTime.secondsOnly": "~{{seconds}} sn", + "bridgingPopularTokens.banner.description": "DOGE, XRP, XLP — now available on Unichain.", "chart.candlestick": "Mum grafiği", "chart.error.pools": "Geçerli havuza ilişkin geçmiş veriler görüntülenemiyor.", "chart.error.tokens": "Geçerli token'ın geçmiş verileri görüntülenemiyor.", @@ -1437,17 +1438,32 @@ "poolFinder.availablePools.notFound.description": "Eşleşen v2 havuzları bulunamadı. Token seçimini bir kez daha kontrol et ve doğru cüzdana bağlı olduğundan emin ol.", "pools.explore": "Havuzları keşfet", "portfolio.activity.filters.timePeriod.all": "Her zaman", + "portfolio.activity.filters.transactionType.addLiquidity": "Add Liquidity", "portfolio.activity.filters.transactionType.all": "Tüm türler", - "portfolio.activity.filters.transactionType.deposits": "Yatırılan", - "portfolio.activity.filters.transactionType.staking": "Staking", + "portfolio.activity.filters.transactionType.approvals": "Approvals", + "portfolio.activity.filters.transactionType.claimFees": "Claim Fees", + "portfolio.activity.filters.transactionType.createPool": "Create Pool", + "portfolio.activity.filters.transactionType.mints": "Mints", + "portfolio.activity.filters.transactionType.removeLiquidity": "Remove Liquidity", "portfolio.activity.filters.transactionType.swaps": "Swap'lar", + "portfolio.activity.filters.transactionType.wraps": "Wraps", + "portfolio.activity.table.column.address": "Address", + "portfolio.activity.table.column.amount": "Amount", + "portfolio.activity.table.column.time": "Time", + "portfolio.activity.table.column.type": "Type", "portfolio.activity.title": "Etkinlik", - "portfolio.connectWallet.summary": "Track tokens, pools, and more across {{amount}}+ networks", "portfolio.defi.title": "DeFi", "portfolio.description": "Kripto para portföyünü tüm zincirlerde ve protokollerde takip et", + "portfolio.disconnected.connectWallet.cta": "Track your portfolio", + "portfolio.disconnected.cta.description": "Track tokens, positions, NFTs and more across {{numNetworks}}+ networks", + "portfolio.disconnected.demoWallet.description": "This is a view-only example. Connect your wallet to manage your portfolio.", + "portfolio.disconnected.demoWallet.title": "Demo wallet", + "portfolio.disconnected.viewYourPortfolio.cta": "to view your portfolio", "portfolio.nfts.title": "NFT'ler", "portfolio.overview.title": "Genel Bakış", "portfolio.title": "Portföy", + "portfolio.tokens.balance.totalTokens": "{{numTokens}} tokens", + "portfolio.tokens.emptyState": "No tokens", "portfolio.tokens.table.column.allocation": "Dağıtım", "portfolio.tokens.table.column.balance": "Bakiye", "portfolio.tokens.table.column.change1d": "1 Günlük Değişim", @@ -2505,6 +2521,12 @@ "uwulink.error.insufficientTokens": "{{chain}} zincirinde yeterli {{tokenSymbol}} yok", "v2.notAvailable": "Uniswap V2 bu ağda mevcut değil.", "wallet.appSignIn": "Uygulama ile giriş yap", + "wallet.connecting.description": "Complete connection in your wallet", + "wallet.connecting.solanaPrompt": "Use Solana on Uniswap", + "wallet.connecting.solanaPrompt.button": "Use Solana", + "wallet.connecting.solanaPrompt.description": "You’ll be asked to connect again in MetaMask", + "wallet.connecting.title.evm": "Connect to {{walletName}}", + "wallet.connecting.title.svm": "Connect to {{walletName}} on Solana", "wallet.connectingAgreement": "Bir cüzdan bağlayarak Uniswap Labs'ın Hizmet Şartlarını ve Gizlilik Politikasını kabul etmiş olursun.", "wallet.connectionFailed.message": "Connection attempt failed. Please try again, following the steps to connect in your wallet.", "wallet.mismatch.popup.description": "Bağlı cüzdanın, bazı özellikleri desteklemiyor.", diff --git a/packages/uniswap/src/i18n/locales/translations/uk-UA.json b/packages/uniswap/src/i18n/locales/translations/uk-UA.json index 147135f6657..2cd48cba7bb 100644 --- a/packages/uniswap/src/i18n/locales/translations/uk-UA.json +++ b/packages/uniswap/src/i18n/locales/translations/uk-UA.json @@ -1675,6 +1675,7 @@ "settings.action.lock": "Заблокувати гаманець", "settings.action.privacy": "Політика конфіденційності", "settings.action.terms": "Умови використання", + "settings.connectWalletPlatform.warning": "Щоб використовувати Uniswap на {{platform}}, підключіться до гаманця, який підтримує {{platform}}.", "settings.footer": "Зроблено з любов’ю, \nкоманда Uniswap 🦄", "settings.hideSmallBalances": "Приховайте невеликі залишки", "settings.hideSmallBalances.subtitle": "Залишки менше 1 долара США будуть приховані з вашого портфеля.", @@ -1845,7 +1846,7 @@ "smartWallets.postSwapNudge.title.dapp": "Цей додаток підтримує розумні гаманці", "smartWallets.unavailableModal.description": "Інший постачальник гаманців тепер керує налаштуваннями смарт-гаманця для {{displayName}}. Ви можете продовжувати користуватися Uniswap як завжди.", "smartWallets.unavailableModal.title": "Функції розумного гаманця недоступні", - "solanaPromo.banner.description": "Торгуйте токенами Solana безпосередньо у веб-додатку Uniswap.", + "solanaPromo.banner.description": "Торгуйте токенами Solana безпосередньо на Uniswap.", "solanaPromo.banner.title": "Солана вже доступна", "solanaPromo.modal.connectWallet": "Підключіть свій улюблений гаманець Solana", "solanaPromo.modal.startSwapping.button": "Почніть обмін на Solana", diff --git a/packages/uniswap/src/i18n/locales/translations/ur-PK.json b/packages/uniswap/src/i18n/locales/translations/ur-PK.json index af9efc42fb9..184ada9065d 100644 --- a/packages/uniswap/src/i18n/locales/translations/ur-PK.json +++ b/packages/uniswap/src/i18n/locales/translations/ur-PK.json @@ -1675,6 +1675,7 @@ "settings.action.lock": "پرس مقفل کریں۔", "settings.action.privacy": "رازداری کی پالیسی", "settings.action.terms": "سروس کی شرائط", + "settings.connectWalletPlatform.warning": "{{platform}}پر Uniswap استعمال کرنے کے لیے، ایک ایسے والیٹ سے جڑیں جو {{platform}}کو سپورٹ کرتا ہو۔", "settings.footer": "محبت کے ساتھ بنایا گیا، \nUnswap ٹیم 🦄", "settings.hideSmallBalances": "چھوٹے بیلنس چھپائیں۔", "settings.hideSmallBalances.subtitle": "1 USD سے کم بیلنس آپ کے پورٹ فولیو سے چھپائے جائیں گے۔", @@ -1845,7 +1846,7 @@ "smartWallets.postSwapNudge.title.dapp": "یہ ایپ سمارٹ بٹوے کو سپورٹ کرتی ہے۔", "smartWallets.unavailableModal.description": "ایک مختلف والیٹ فراہم کنندہ اب {{displayName}}کے لیے سمارٹ والیٹ کی ترتیبات کا انتظام کر رہا ہے۔ آپ یونی سویپ کا استعمال معمول کے مطابق جاری رکھ سکتے ہیں۔", "smartWallets.unavailableModal.title": "اسمارٹ والیٹ کی خصوصیات دستیاب نہیں ہیں۔", - "solanaPromo.banner.description": "سولانا ٹوکنز کو براہ راست Uniswap ویب ایپ پر تجارت کریں۔", + "solanaPromo.banner.description": "سولانا ٹوکنز کو براہ راست Uniswap پر تجارت کریں۔", "solanaPromo.banner.title": "سولانا اب دستیاب ہے۔", "solanaPromo.modal.connectWallet": "اپنے پسندیدہ سولانا والیٹ کو جوڑیں۔", "solanaPromo.modal.startSwapping.button": "سولانا پر تبادلہ کرنا شروع کریں۔", diff --git a/packages/uniswap/src/i18n/locales/translations/vi-VN.json b/packages/uniswap/src/i18n/locales/translations/vi-VN.json index dd72bb83886..ea0d816cc7e 100644 --- a/packages/uniswap/src/i18n/locales/translations/vi-VN.json +++ b/packages/uniswap/src/i18n/locales/translations/vi-VN.json @@ -152,6 +152,7 @@ "bridging.estimatedTime.minutesAndSeconds": "~{{minutes}} phút {{seconds}} giây", "bridging.estimatedTime.minutesOnly": "~{{minutes}} phút", "bridging.estimatedTime.secondsOnly": "~{{seconds}} giây", + "bridgingPopularTokens.banner.description": "DOGE, XRP, XLP — now available on Unichain.", "chart.candlestick": "Biểu đồ nến", "chart.error.pools": "Không thể hiển thị dữ liệu lịch sử cho pool hiện tại.", "chart.error.tokens": "Không thể hiển thị dữ liệu lịch sử cho token hiện tại.", @@ -1437,17 +1438,32 @@ "poolFinder.availablePools.notFound.description": "Không tìm thấy pool v2 trùng khớp. Hãy kiểm tra lại lựa chọn token của bạn và đảm bảo bạn đã kết nối với đúng ví.", "pools.explore": "Khám phá pool", "portfolio.activity.filters.timePeriod.all": "Mọi thời điểm", + "portfolio.activity.filters.transactionType.addLiquidity": "Add Liquidity", "portfolio.activity.filters.transactionType.all": "Mọi loại", - "portfolio.activity.filters.transactionType.deposits": "Các giao dịch nạp", - "portfolio.activity.filters.transactionType.staking": "Staking", + "portfolio.activity.filters.transactionType.approvals": "Approvals", + "portfolio.activity.filters.transactionType.claimFees": "Claim Fees", + "portfolio.activity.filters.transactionType.createPool": "Create Pool", + "portfolio.activity.filters.transactionType.mints": "Mints", + "portfolio.activity.filters.transactionType.removeLiquidity": "Remove Liquidity", "portfolio.activity.filters.transactionType.swaps": "Các giao dịch hoán đổi", + "portfolio.activity.filters.transactionType.wraps": "Wraps", + "portfolio.activity.table.column.address": "Address", + "portfolio.activity.table.column.amount": "Amount", + "portfolio.activity.table.column.time": "Time", + "portfolio.activity.table.column.type": "Type", "portfolio.activity.title": "Hoạt động", - "portfolio.connectWallet.summary": "Track tokens, pools, and more across {{amount}}+ networks", "portfolio.defi.title": "DeFi", "portfolio.description": "Theo dõi danh mục đầu tư crypto của bạn trên tất cả các blockchain và giao thức", + "portfolio.disconnected.connectWallet.cta": "Track your portfolio", + "portfolio.disconnected.cta.description": "Track tokens, positions, NFTs and more across {{numNetworks}}+ networks", + "portfolio.disconnected.demoWallet.description": "This is a view-only example. Connect your wallet to manage your portfolio.", + "portfolio.disconnected.demoWallet.title": "Demo wallet", + "portfolio.disconnected.viewYourPortfolio.cta": "to view your portfolio", "portfolio.nfts.title": "NFT", "portfolio.overview.title": "Tổng quan", "portfolio.title": "Danh mục đầu tư", + "portfolio.tokens.balance.totalTokens": "{{numTokens}} tokens", + "portfolio.tokens.emptyState": "No tokens", "portfolio.tokens.table.column.allocation": "Phân bổ", "portfolio.tokens.table.column.balance": "Số dư", "portfolio.tokens.table.column.change1d": "Biến động trong 1 ngày", @@ -2505,6 +2521,12 @@ "uwulink.error.insufficientTokens": "Không đủ {{tokenSymbol}} trên {{chain}}", "v2.notAvailable": "Uniswap V2 không khả dụng trên mạng này.", "wallet.appSignIn": "Đăng nhập bằng ứng dụng", + "wallet.connecting.description": "Complete connection in your wallet", + "wallet.connecting.solanaPrompt": "Use Solana on Uniswap", + "wallet.connecting.solanaPrompt.button": "Use Solana", + "wallet.connecting.solanaPrompt.description": "You’ll be asked to connect again in MetaMask", + "wallet.connecting.title.evm": "Connect to {{walletName}}", + "wallet.connecting.title.svm": "Connect to {{walletName}} on Solana", "wallet.connectingAgreement": "Bằng việc kết nối ví, bạn đồng ý với Điều khoản dịch vụ của Uniswap Labs và chấp nhận Chính sách về quyền riêng tư của Uniswap Labs.", "wallet.connectionFailed.message": "Connection attempt failed. Please try again, following the steps to connect in your wallet.", "wallet.mismatch.popup.description": "Ví đã kết nối của bạn không hỗ trợ một số tính năng.", diff --git a/packages/uniswap/src/i18n/locales/translations/zh-CN.json b/packages/uniswap/src/i18n/locales/translations/zh-CN.json index 4822b97d6f4..17ad3bb5632 100644 --- a/packages/uniswap/src/i18n/locales/translations/zh-CN.json +++ b/packages/uniswap/src/i18n/locales/translations/zh-CN.json @@ -152,6 +152,7 @@ "bridging.estimatedTime.minutesAndSeconds": "~{{minutes}} 分钟 {{seconds}} 秒", "bridging.estimatedTime.minutesOnly": "~{{minutes}} 分钟", "bridging.estimatedTime.secondsOnly": "~{{seconds}} 秒", + "bridgingPopularTokens.banner.description": "DOGE, XRP, XLP — now available on Unichain.", "chart.candlestick": "K 线图", "chart.error.pools": "无法显示当前资金池的历史数据。", "chart.error.tokens": "无法显示当前代币的历史数据。", @@ -1437,17 +1438,32 @@ "poolFinder.availablePools.notFound.description": "未找到相符的 v2 资金池。请仔细检查你选择的代币,并确保你已连接至正确的钱包。", "pools.explore": "探索资金池", "portfolio.activity.filters.timePeriod.all": "所有时间", + "portfolio.activity.filters.transactionType.addLiquidity": "Add Liquidity", "portfolio.activity.filters.transactionType.all": "所有类型", - "portfolio.activity.filters.transactionType.deposits": "存入", - "portfolio.activity.filters.transactionType.staking": "质押", + "portfolio.activity.filters.transactionType.approvals": "Approvals", + "portfolio.activity.filters.transactionType.claimFees": "Claim Fees", + "portfolio.activity.filters.transactionType.createPool": "Create Pool", + "portfolio.activity.filters.transactionType.mints": "Mints", + "portfolio.activity.filters.transactionType.removeLiquidity": "Remove Liquidity", "portfolio.activity.filters.transactionType.swaps": "交换", + "portfolio.activity.filters.transactionType.wraps": "Wraps", + "portfolio.activity.table.column.address": "Address", + "portfolio.activity.table.column.amount": "Amount", + "portfolio.activity.table.column.time": "Time", + "portfolio.activity.table.column.type": "Type", "portfolio.activity.title": "活动", - "portfolio.connectWallet.summary": "Track tokens, pools, and more across {{amount}}+ networks", "portfolio.defi.title": "DeFi", "portfolio.description": "跨所有区块链和协议追踪你的加密货币资产组合", + "portfolio.disconnected.connectWallet.cta": "Track your portfolio", + "portfolio.disconnected.cta.description": "Track tokens, positions, NFTs and more across {{numNetworks}}+ networks", + "portfolio.disconnected.demoWallet.description": "This is a view-only example. Connect your wallet to manage your portfolio.", + "portfolio.disconnected.demoWallet.title": "Demo wallet", + "portfolio.disconnected.viewYourPortfolio.cta": "to view your portfolio", "portfolio.nfts.title": "非同质化代币", "portfolio.overview.title": "概览", "portfolio.title": "资产组合", + "portfolio.tokens.balance.totalTokens": "{{numTokens}} tokens", + "portfolio.tokens.emptyState": "No tokens", "portfolio.tokens.table.column.allocation": "配额", "portfolio.tokens.table.column.balance": "余额", "portfolio.tokens.table.column.change1d": "24 小时变动", @@ -2505,6 +2521,12 @@ "uwulink.error.insufficientTokens": "{{chain}} 上的 {{tokenSymbol}} 不足", "v2.notAvailable": "Uniswap V2 在此网络上不可用。", "wallet.appSignIn": "使用应用登录", + "wallet.connecting.description": "Complete connection in your wallet", + "wallet.connecting.solanaPrompt": "Use Solana on Uniswap", + "wallet.connecting.solanaPrompt.button": "Use Solana", + "wallet.connecting.solanaPrompt.description": "You’ll be asked to connect again in MetaMask", + "wallet.connecting.title.evm": "Connect to {{walletName}}", + "wallet.connecting.title.svm": "Connect to {{walletName}} on Solana", "wallet.connectingAgreement": "通过连接钱包,表明你同意 Uniswap 实验室 的服务条款及其隐私政策。", "wallet.connectionFailed.message": "Connection attempt failed. Please try again, following the steps to connect in your wallet.", "wallet.mismatch.popup.description": "某些功能你的联网钱包不支持。", diff --git a/packages/uniswap/src/i18n/locales/translations/zh-TW.json b/packages/uniswap/src/i18n/locales/translations/zh-TW.json index 43410fc3108..21c0ea4913e 100644 --- a/packages/uniswap/src/i18n/locales/translations/zh-TW.json +++ b/packages/uniswap/src/i18n/locales/translations/zh-TW.json @@ -152,6 +152,7 @@ "bridging.estimatedTime.minutesAndSeconds": "~{{minutes}} 分 {{seconds}} 秒", "bridging.estimatedTime.minutesOnly": "~{{minutes}} 分鐘", "bridging.estimatedTime.secondsOnly": "~{{seconds}} 秒", + "bridgingPopularTokens.banner.description": "DOGE, XRP, XLP — now available on Unichain.", "chart.candlestick": "K 線圖表", "chart.error.pools": "無法顯示目前資產池的過往記錄資料。", "chart.error.tokens": "無法顯示目前代幣的過往記錄資料。", @@ -1437,17 +1438,32 @@ "poolFinder.availablePools.notFound.description": "未找到相符的 v2 資產池。請仔細檢查你選取的代幣,並確保你已連接至正確的錢包。", "pools.explore": "探索資產池", "portfolio.activity.filters.timePeriod.all": "全部時間", + "portfolio.activity.filters.transactionType.addLiquidity": "Add Liquidity", "portfolio.activity.filters.transactionType.all": "所有類型", - "portfolio.activity.filters.transactionType.deposits": "存入", - "portfolio.activity.filters.transactionType.staking": "質押", + "portfolio.activity.filters.transactionType.approvals": "Approvals", + "portfolio.activity.filters.transactionType.claimFees": "Claim Fees", + "portfolio.activity.filters.transactionType.createPool": "Create Pool", + "portfolio.activity.filters.transactionType.mints": "Mints", + "portfolio.activity.filters.transactionType.removeLiquidity": "Remove Liquidity", "portfolio.activity.filters.transactionType.swaps": "交換", + "portfolio.activity.filters.transactionType.wraps": "Wraps", + "portfolio.activity.table.column.address": "Address", + "portfolio.activity.table.column.amount": "Amount", + "portfolio.activity.table.column.time": "Time", + "portfolio.activity.table.column.type": "Type", "portfolio.activity.title": "活動", - "portfolio.connectWallet.summary": "Track tokens, pools, and more across {{amount}}+ networks", "portfolio.defi.title": "DeFi", "portfolio.description": "跨所有鏈和協定追蹤您的加密貨幣資產組合", + "portfolio.disconnected.connectWallet.cta": "Track your portfolio", + "portfolio.disconnected.cta.description": "Track tokens, positions, NFTs and more across {{numNetworks}}+ networks", + "portfolio.disconnected.demoWallet.description": "This is a view-only example. Connect your wallet to manage your portfolio.", + "portfolio.disconnected.demoWallet.title": "Demo wallet", + "portfolio.disconnected.viewYourPortfolio.cta": "to view your portfolio", "portfolio.nfts.title": "NFT", "portfolio.overview.title": "概覽", "portfolio.title": "資產組合", + "portfolio.tokens.balance.totalTokens": "{{numTokens}} tokens", + "portfolio.tokens.emptyState": "No tokens", "portfolio.tokens.table.column.allocation": "配額", "portfolio.tokens.table.column.balance": "餘額", "portfolio.tokens.table.column.change1d": "1 日變動", @@ -2505,6 +2521,12 @@ "uwulink.error.insufficientTokens": "{{chain}} 上的 {{tokenSymbol}} 不足", "v2.notAvailable": "Uniswap V2 在此網路上不適用。", "wallet.appSignIn": "使用 App 登入", + "wallet.connecting.description": "Complete connection in your wallet", + "wallet.connecting.solanaPrompt": "Use Solana on Uniswap", + "wallet.connecting.solanaPrompt.button": "Use Solana", + "wallet.connecting.solanaPrompt.description": "You’ll be asked to connect again in MetaMask", + "wallet.connecting.title.evm": "Connect to {{walletName}}", + "wallet.connecting.title.svm": "Connect to {{walletName}} on Solana", "wallet.connectingAgreement": "連線錢包即表示你同意 Uniswap Labs 的服務條款並同意其隱私權政策。", "wallet.connectionFailed.message": "Connection attempt failed. Please try again, following the steps to connect in your wallet.", "wallet.mismatch.popup.description": "你的已連接錢包不支援某些功能。", diff --git a/packages/uniswap/src/state/oldTypes.ts b/packages/uniswap/src/state/oldTypes.ts index c78dd73667e..964e7c1d065 100644 --- a/packages/uniswap/src/state/oldTypes.ts +++ b/packages/uniswap/src/state/oldTypes.ts @@ -1,4 +1,4 @@ -import { ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' +import { ProtocolVersion } from '@uniswap/client-data-api/dist/data/v1/poolTypes_pb' import { GraphQLApi } from '@universe/api' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { SafetyInfo } from 'uniswap/src/features/dataApi/types' diff --git a/packages/uniswap/src/test/fixtures/gql/assets/tokens.ts b/packages/uniswap/src/test/fixtures/gql/assets/tokens.ts index ef474c92660..708ed37422c 100644 --- a/packages/uniswap/src/test/fixtures/gql/assets/tokens.ts +++ b/packages/uniswap/src/test/fixtures/gql/assets/tokens.ts @@ -58,6 +58,8 @@ export const token = createFixture({ sellFeeBps: '', }, protectionInfo, + isBridged: undefined, + bridgedWithdrawalInfo: undefined, })) export const tokenBalance = createFixture()(() => ({ diff --git a/packages/uniswap/src/test/fixtures/testIDs.ts b/packages/uniswap/src/test/fixtures/testIDs.ts index 4536dff1a7a..dc0ef02f8b1 100644 --- a/packages/uniswap/src/test/fixtures/testIDs.ts +++ b/packages/uniswap/src/test/fixtures/testIDs.ts @@ -69,6 +69,7 @@ export const TestID = { ExploreFilterChainPrefix: 'explore-filter-chain-', ExploreSearchInput: 'explore-search-input', ExploreSortButton: 'explore-sort-button', + ExploreTab: 'explore-tab', ExploreSortByVolume: 'explore-sort-by-volume', ExploreTokensSearchInput: 'explore-tokens-search-input', Favorite: 'favorite', @@ -81,6 +82,7 @@ export const TestID = { HiddenNftsRow: 'hidden-nfts-row', HelpIcon: 'help-icon', HelpModal: 'help-modal', + HomeTab: 'home-tab', ImportAccount: 'import-account', ImportAccountInput: 'import-account-input', InvertPrice: 'invert-price', diff --git a/packages/uniswap/src/test/mocks/gql/mocks.ts b/packages/uniswap/src/test/mocks/gql/mocks.ts index ec307734bc0..894a005e0ae 100644 --- a/packages/uniswap/src/test/mocks/gql/mocks.ts +++ b/packages/uniswap/src/test/mocks/gql/mocks.ts @@ -28,6 +28,8 @@ export const mocks = { symbol: () => faker.lorem.word(), protectionInfo: () => ({ result: randomEnumValue(GraphQLApi.ProtectionResult), attackTypes: [] }), feeData: () => ({ buyFeeBps: '', sellFeeBps: '' }), + isBridged: () => null, + bridgedWithdrawalInfo: () => null, }, Amount: { id: () => faker.datatype.uuid(), diff --git a/packages/uniswap/src/utils/datadog.web.ts b/packages/uniswap/src/utils/datadog.web.ts index 3da89dd1680..fd79ee7cc6e 100644 --- a/packages/uniswap/src/utils/datadog.web.ts +++ b/packages/uniswap/src/utils/datadog.web.ts @@ -1,17 +1,18 @@ import { datadogLogs } from '@datadog/browser-logs' import { datadogRum, RumEvent, RumEventDomainContext, RumFetchResourceEventDomainContext } from '@datadog/browser-rum' -import { config } from 'uniswap/src/config' import { DatadogIgnoredErrorsConfigKey, DatadogIgnoredErrorsValType, DatadogSessionSampleRateKey, DatadogSessionSampleRateValType, DynamicConfigs, -} from 'uniswap/src/features/gating/configs' -import { Experiments } from 'uniswap/src/features/gating/experiments' -import { WALLET_FEATURE_FLAG_NAMES, WEB_FEATURE_FLAG_NAMES } from 'uniswap/src/features/gating/flags' -import { getDynamicConfigValue } from 'uniswap/src/features/gating/hooks' -import { getStatsigClient } from 'uniswap/src/features/gating/sdk/statsig' + Experiments, + getDynamicConfigValue, + getStatsigClient, + WALLET_FEATURE_FLAG_NAMES, + WEB_FEATURE_FLAG_NAMES, +} from '@universe/gating' +import { config } from 'uniswap/src/config' import { getUniqueId } from 'utilities/src/device/uniqueId' import { datadogEnabledBuild, localDevDatadogEnabled } from 'utilities/src/environment/constants' import { isBetaEnv } from 'utilities/src/environment/env' @@ -40,7 +41,9 @@ function beforeSend(event: RumEvent, context: RumEventDomainContext): boolean { defaultValue: [], }) - const ignoredError = ignoredErrors.find(({ messageContains }) => event.error.message.includes(messageContains)) + const ignoredError = ignoredErrors.find(({ messageContains }: { messageContains: string }) => + event.error.message.includes(messageContains), + ) if (ignoredError && Math.random() > ignoredError.sampleRate) { return false } diff --git a/packages/uniswap/src/utils/search/doesTokenMatchSearchTerm.test.ts b/packages/uniswap/src/utils/search/doesTokenMatchSearchTerm.test.ts new file mode 100644 index 00000000000..f04ac41d388 --- /dev/null +++ b/packages/uniswap/src/utils/search/doesTokenMatchSearchTerm.test.ts @@ -0,0 +1,419 @@ +import { Currency } from '@uniswap/sdk-core' +import { DAI, nativeOnChain, USDC, WBTC } from 'uniswap/src/constants/tokens' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { doesTokenMatchSearchTerm } from 'uniswap/src/utils/search/doesTokenMatchSearchTerm' + +// Test data factory functions using real tokens +const createMockCurrencyInfo = ( + overrides: Partial<{ currencyId: string; currency: Currency }> = {}, +): { currencyId: string; currency: Currency } => ({ + currencyId: 'TEST', + currency: USDC, // Default to USDC + ...overrides, +}) + +const createMockTokenWithInfo = ( + overrides: Partial<{ currencyInfo: { currencyId: string; currency: Currency } | null }> = {}, +): { currencyInfo: { currencyId: string; currency: Currency } | null } => ({ + currencyInfo: createMockCurrencyInfo(), + ...overrides, +}) + +describe('doesTokenMatchSearchTerm', () => { + describe('when searchTerm is empty or undefined', () => { + it('should return true when searchTerm is undefined', () => { + const token = createMockTokenWithInfo() + + const result = doesTokenMatchSearchTerm(token, undefined as any) + + expect(result).toBe(true) + }) + + it('should return true when searchTerm is null', () => { + const token = createMockTokenWithInfo() + + const result = doesTokenMatchSearchTerm(token, null as any) + + expect(result).toBe(true) + }) + + it('should return true when searchTerm is empty string', () => { + const token = createMockTokenWithInfo() + + const result = doesTokenMatchSearchTerm(token, '') + + expect(result).toBe(true) + }) + + it('should return true when searchTerm is only whitespace', () => { + const token = createMockTokenWithInfo() + + const result = doesTokenMatchSearchTerm(token, ' ') + + expect(result).toBe(true) + }) + + it('should return true when searchTerm is only tabs and newlines', () => { + const token = createMockTokenWithInfo() + + const result = doesTokenMatchSearchTerm(token, '\t\n\r ') + + expect(result).toBe(true) + }) + }) + + describe('when currencyInfo is null', () => { + it('should return false when currencyInfo is null', () => { + const token = createMockTokenWithInfo({ + currencyInfo: null, + }) + + const result = doesTokenMatchSearchTerm(token, 'test') + + expect(result).toBe(false) + }) + }) + + describe('when searching by token name', () => { + it('should match when search term is in token name (case insensitive)', () => { + const token = createMockTokenWithInfo({ + currencyInfo: createMockCurrencyInfo({ + currency: DAI, // DAI has name "Dai Stablecoin" + }), + }) + + const result = doesTokenMatchSearchTerm(token, 'dai') + + expect(result).toBe(true) + }) + + it('should match when search term is in token name with different case', () => { + const token = createMockTokenWithInfo({ + currencyInfo: createMockCurrencyInfo({ + currency: DAI, // DAI has name "Dai Stablecoin" + }), + }) + + const result = doesTokenMatchSearchTerm(token, 'STABLECOIN') + + expect(result).toBe(true) + }) + + it('should not match when search term is not in token name', () => { + const token = createMockTokenWithInfo({ + currencyInfo: createMockCurrencyInfo({ + currency: USDC, // USDC has name "USD Coin" + }), + }) + + const result = doesTokenMatchSearchTerm(token, 'bitcoin') + + expect(result).toBe(false) + }) + + it('should handle undefined token name', () => { + // Create a token with undefined name by modifying WBTC + const tokenWithUndefinedName = { + ...WBTC, + name: undefined, + } as any + + const token = createMockTokenWithInfo({ + currencyInfo: createMockCurrencyInfo({ + currency: tokenWithUndefinedName, + }), + }) + + const result = doesTokenMatchSearchTerm(token, 'test') + + expect(result).toBe(false) + }) + }) + + describe('when searching by token symbol', () => { + it('should match when search term is in token symbol (case insensitive)', () => { + const token = createMockTokenWithInfo({ + currencyInfo: createMockCurrencyInfo({ + currency: USDC, // USDC has symbol "USDC" + }), + }) + + const result = doesTokenMatchSearchTerm(token, 'usd') + + expect(result).toBe(true) + }) + + it('should match when search term is in token symbol with different case', () => { + const token = createMockTokenWithInfo({ + currencyInfo: createMockCurrencyInfo({ + currency: USDC, // USDC has symbol "USDC" + }), + }) + + const result = doesTokenMatchSearchTerm(token, 'USDC') + + expect(result).toBe(true) + }) + + it('should not match when search term is not in token symbol', () => { + const token = createMockTokenWithInfo({ + currencyInfo: createMockCurrencyInfo({ + currency: USDC, // USDC has symbol "USDC" + }), + }) + + const result = doesTokenMatchSearchTerm(token, 'bitcoin') + + expect(result).toBe(false) + }) + + it('should handle undefined token symbol', () => { + // Create a token with undefined symbol by modifying WBTC + const tokenWithUndefinedSymbol = { + ...WBTC, + symbol: undefined, + } as any + + const token = createMockTokenWithInfo({ + currencyInfo: createMockCurrencyInfo({ + currency: tokenWithUndefinedSymbol, + }), + }) + + const result = doesTokenMatchSearchTerm(token, 'test') + + expect(result).toBe(false) + }) + }) + + describe('when searching by token address', () => { + it('should match when search term is in token address (case insensitive)', () => { + const token = createMockTokenWithInfo({ + currencyInfo: createMockCurrencyInfo({ + currency: USDC, // USDC has address starting with 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 + }), + }) + + const result = doesTokenMatchSearchTerm(token, 'a0b8') + + expect(result).toBe(true) + }) + + it('should match when search term is in token address with different case', () => { + const token = createMockTokenWithInfo({ + currencyInfo: createMockCurrencyInfo({ + currency: USDC, // USDC has address starting with 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 + }), + }) + + const result = doesTokenMatchSearchTerm(token, 'A0B8') + + expect(result).toBe(true) + }) + + it('should not match when search term is not in token address', () => { + const token = createMockTokenWithInfo({ + currencyInfo: createMockCurrencyInfo({ + currency: USDC, + }), + }) + + const result = doesTokenMatchSearchTerm(token, '9999') + + expect(result).toBe(false) + }) + + it('should not search by address for native currencies', () => { + const token = createMockTokenWithInfo({ + currencyInfo: createMockCurrencyInfo({ + currency: nativeOnChain(UniverseChainId.Mainnet), // Native ETH + }), + }) + + const result = doesTokenMatchSearchTerm(token, '0x') + + expect(result).toBe(false) + }) + }) + + describe('when multiple fields match', () => { + it('should return true if name matches even if symbol and address do not', () => { + const token = createMockTokenWithInfo({ + currencyInfo: createMockCurrencyInfo({ + currency: DAI, // DAI has name "Dai Stablecoin" and symbol "DAI" + }), + }) + + const result = doesTokenMatchSearchTerm(token, 'dai') + + expect(result).toBe(true) + }) + + it('should return true if symbol matches even if name and address do not', () => { + const token = createMockTokenWithInfo({ + currencyInfo: createMockCurrencyInfo({ + currency: USDC, // USDC has symbol "USDC" and name "USD Coin" + }), + }) + + const result = doesTokenMatchSearchTerm(token, 'usd') + + expect(result).toBe(true) + }) + + it('should return true if address matches even if name and symbol do not', () => { + const token = createMockTokenWithInfo({ + currencyInfo: createMockCurrencyInfo({ + currency: USDC, // USDC has address starting with 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 + }), + }) + + const result = doesTokenMatchSearchTerm(token, 'a0b8') + + expect(result).toBe(true) + }) + }) + + describe('edge cases', () => { + it('should handle partial matches at the beginning of strings', () => { + const token = createMockTokenWithInfo({ + currencyInfo: createMockCurrencyInfo({ + currency: DAI, // DAI has symbol "DAI" + }), + }) + + const result = doesTokenMatchSearchTerm(token, 'dai') + + expect(result).toBe(true) + }) + + it('should handle partial matches at the end of strings', () => { + const token = createMockTokenWithInfo({ + currencyInfo: createMockCurrencyInfo({ + currency: DAI, // DAI has name "Dai Stablecoin" + }), + }) + + const result = doesTokenMatchSearchTerm(token, 'coin') + + expect(result).toBe(true) + }) + + it('should handle partial matches in the middle of strings', () => { + // Fix the imported WBTC token properties + const wbtcToken = { + ...WBTC, + name: 'Wrapped BTC', + symbol: 'WBTC', + } as any + + const token = createMockTokenWithInfo({ + currencyInfo: createMockCurrencyInfo({ + currency: wbtcToken, + }), + }) + + const result = doesTokenMatchSearchTerm(token, 'wrapped') + + expect(result).toBe(true) + }) + + it('should handle special characters in search term', () => { + const token = createMockTokenWithInfo({ + currencyInfo: createMockCurrencyInfo({ + currency: USDC, // USDC has name "USD Coin" + }), + }) + + const result = doesTokenMatchSearchTerm(token, 'usd') + + expect(result).toBe(true) + }) + + it('should handle very long search terms', () => { + const token = createMockTokenWithInfo({ + currencyInfo: createMockCurrencyInfo({ + currency: USDC, + }), + }) + + const longSearchTerm = 'a'.repeat(1000) + const result = doesTokenMatchSearchTerm(token, longSearchTerm) + + expect(result).toBe(false) + }) + + it('should handle empty token name and symbol', () => { + // Create a token with empty name and symbol by modifying WBTC + const tokenWithEmptyFields = { + ...WBTC, + name: '', + symbol: '', + } as any + + const token = createMockTokenWithInfo({ + currencyInfo: createMockCurrencyInfo({ + currency: tokenWithEmptyFields, + }), + }) + + const result = doesTokenMatchSearchTerm(token, 'test') + + expect(result).toBe(false) + }) + }) + + describe('with different currency types', () => { + it('should work with Token instances', () => { + const token = createMockTokenWithInfo({ + currencyInfo: createMockCurrencyInfo({ + currency: USDC, // USDC is a Token instance + }), + }) + + const result = doesTokenMatchSearchTerm(token, 'usd') + + expect(result).toBe(true) + }) + + it('should work with NativeCurrency instances', () => { + const token = createMockTokenWithInfo({ + currencyInfo: createMockCurrencyInfo({ + currency: nativeOnChain(UniverseChainId.Mainnet), // Native ETH + }), + }) + + const result = doesTokenMatchSearchTerm(token, 'ethereum') + + expect(result).toBe(true) + }) + + it('should not search by address for NativeCurrency', () => { + const token = createMockTokenWithInfo({ + currencyInfo: createMockCurrencyInfo({ + currency: nativeOnChain(UniverseChainId.Mainnet), // Native ETH + }), + }) + + // NativeCurrency doesn't have an address, so this should not match + const result = doesTokenMatchSearchTerm(token, '0x') + + expect(result).toBe(false) + }) + }) + + describe('case sensitivity', () => { + it('should be case insensitive for all fields', () => { + const token = createMockTokenWithInfo({ + currencyInfo: createMockCurrencyInfo({ + currency: USDC, // USDC has name "USD Coin" and symbol "USDC" + }), + }) + + expect(doesTokenMatchSearchTerm(token, 'usd')).toBe(true) + expect(doesTokenMatchSearchTerm(token, 'USD')).toBe(true) + expect(doesTokenMatchSearchTerm(token, 'Usd')).toBe(true) + expect(doesTokenMatchSearchTerm(token, 'UsD')).toBe(true) + }) + }) +}) diff --git a/packages/uniswap/src/utils/search/doesTokenMatchSearchTerm.ts b/packages/uniswap/src/utils/search/doesTokenMatchSearchTerm.ts new file mode 100644 index 00000000000..f056a6f7c2a --- /dev/null +++ b/packages/uniswap/src/utils/search/doesTokenMatchSearchTerm.ts @@ -0,0 +1,37 @@ +import { Currency, Token } from '@uniswap/sdk-core' + +/** + * Checks if a token matches a search term. + * + * @param token - The token to check + * @param searchTerm - The search term to match against + * @returns True if the token matches the search term, false otherwise + */ +export function doesTokenMatchSearchTerm( + token: { currencyInfo: { currencyId: string; currency: Currency } | null }, + searchTerm: string, +): boolean { + if (!searchTerm || !searchTerm.trim()) { + return true + } + + const lowercaseSearch = searchTerm.toLowerCase() + + const currencyInfo = token.currencyInfo + if (!currencyInfo) { + return false + } + const currency = currencyInfo.currency + + // Search by token name + const nameIncludesSearch = currency.name?.toLowerCase().includes(lowercaseSearch) + + // Search by token symbol + const symbolIncludesSearch = currency.symbol?.toLowerCase().includes(lowercaseSearch) + + // Search by token address (normalized for consistency with explore page) + const addressIncludesSearch = + currency instanceof Token ? currency.address.toLowerCase().includes(lowercaseSearch) : false + + return Boolean(nameIncludesSearch || symbolIncludesSearch || addressIncludesSearch) +} diff --git a/packages/uniswap/src/utils/search/getPossibleChainMatchFromSearchWord.test.ts b/packages/uniswap/src/utils/search/getPossibleChainMatchFromSearchWord.test.ts new file mode 100644 index 00000000000..af0cfc67144 --- /dev/null +++ b/packages/uniswap/src/utils/search/getPossibleChainMatchFromSearchWord.test.ts @@ -0,0 +1,437 @@ +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { getPossibleChainMatchFromSearchWord } from 'uniswap/src/utils/search/getPossibleChainMatchFromSearchWord' + +// Mock the dependencies before importing the function +jest.mock('uniswap/src/features/chains/chainInfo', () => ({ + getChainInfo: jest.fn(), +})) + +jest.mock('uniswap/src/features/chains/utils', () => ({ + isTestnetChain: jest.fn(), +})) + +// Import the mocked functions +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { isTestnetChain } from 'uniswap/src/features/chains/utils' + +const mockGetChainInfo = getChainInfo as any +const mockIsTestnetChain = isTestnetChain as any + +describe('getPossibleChainMatchFromSearchWord', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + describe('when search word is empty or invalid', () => { + it('should return undefined when search word is empty string', () => { + const enabledChains = [UniverseChainId.Mainnet, UniverseChainId.Polygon] + + const result = getPossibleChainMatchFromSearchWord('', enabledChains) + + expect(result).toBeUndefined() + }) + + it('should return undefined when search word is null', () => { + const enabledChains = [UniverseChainId.Mainnet, UniverseChainId.Polygon] + + const result = getPossibleChainMatchFromSearchWord(null as any, enabledChains) + + expect(result).toBeUndefined() + }) + + it('should return undefined when search word is undefined', () => { + const enabledChains = [UniverseChainId.Mainnet, UniverseChainId.Polygon] + + const result = getPossibleChainMatchFromSearchWord(undefined as any, enabledChains) + + expect(result).toBeUndefined() + }) + + it('should return undefined when search word is only whitespace', () => { + const enabledChains = [UniverseChainId.Mainnet, UniverseChainId.Polygon] + + // Mock the functions to return proper structure + mockIsTestnetChain.mockReturnValue(false) + mockGetChainInfo.mockImplementation((chainId: UniverseChainId) => { + if (chainId === UniverseChainId.Mainnet) { + return { + nativeCurrency: { name: 'Ethereum' }, + interfaceName: 'mainnet', + } as any + } + if (chainId === UniverseChainId.Polygon) { + return { + nativeCurrency: { name: 'Polygon' }, + interfaceName: 'polygon', + } as any + } + return {} as any + }) + + const result = getPossibleChainMatchFromSearchWord(' ', enabledChains) + + expect(result).toBeUndefined() + }) + }) + + describe('when matching by native currency name', () => { + it('should match exact native currency name', () => { + const enabledChains = [UniverseChainId.Mainnet, UniverseChainId.Polygon] + + mockIsTestnetChain.mockReturnValue(false) + mockGetChainInfo.mockImplementation((chainId: UniverseChainId) => { + if (chainId === UniverseChainId.Mainnet) { + return { + nativeCurrency: { name: 'Ethereum' }, + interfaceName: 'mainnet', + } as any + } + if (chainId === UniverseChainId.Polygon) { + return { + nativeCurrency: { name: 'Polygon' }, + interfaceName: 'polygon', + } as any + } + return {} as any + }) + + const result = getPossibleChainMatchFromSearchWord('ethereum', enabledChains) + + expect(result).toBe(UniverseChainId.Mainnet) + }) + + it('should be case insensitive for native currency name', () => { + const enabledChains = [UniverseChainId.Mainnet, UniverseChainId.Polygon] + + mockIsTestnetChain.mockReturnValue(false) + mockGetChainInfo.mockImplementation((chainId: UniverseChainId) => { + if (chainId === UniverseChainId.Mainnet) { + return { + nativeCurrency: { name: 'Ethereum' }, + interfaceName: 'mainnet', + } as any + } + if (chainId === UniverseChainId.Polygon) { + return { + nativeCurrency: { name: 'Polygon' }, + interfaceName: 'polygon', + } as any + } + return {} as any + }) + + const result = getPossibleChainMatchFromSearchWord('ETHEREUM', enabledChains) + + expect(result).toBe(UniverseChainId.Mainnet) + }) + }) + + describe('when matching by interface name', () => { + it('should match exact interface name', () => { + const enabledChains = [UniverseChainId.Mainnet, UniverseChainId.Polygon] + + mockIsTestnetChain.mockReturnValue(false) + mockGetChainInfo.mockImplementation((chainId: UniverseChainId) => { + if (chainId === UniverseChainId.Mainnet) { + return { + nativeCurrency: { name: 'Ethereum' }, + interfaceName: 'mainnet', + } as any + } + if (chainId === UniverseChainId.Polygon) { + return { + nativeCurrency: { name: 'Polygon' }, + interfaceName: 'polygon', + } as any + } + return {} as any + }) + + const result = getPossibleChainMatchFromSearchWord('mainnet', enabledChains) + + expect(result).toBe(UniverseChainId.Mainnet) + }) + + it('should be case insensitive for interface name', () => { + const enabledChains = [UniverseChainId.Mainnet, UniverseChainId.Polygon] + + mockIsTestnetChain.mockReturnValue(false) + mockGetChainInfo.mockImplementation((chainId: UniverseChainId) => { + if (chainId === UniverseChainId.Mainnet) { + return { + nativeCurrency: { name: 'Ethereum' }, + interfaceName: 'mainnet', + } as any + } + if (chainId === UniverseChainId.Polygon) { + return { + nativeCurrency: { name: 'Polygon' }, + interfaceName: 'polygon', + } as any + } + return {} as any + }) + + const result = getPossibleChainMatchFromSearchWord('MAINNET', enabledChains) + + expect(result).toBe(UniverseChainId.Mainnet) + }) + + it('should match polygon interface name', () => { + const enabledChains = [UniverseChainId.Mainnet, UniverseChainId.Polygon] + + mockIsTestnetChain.mockReturnValue(false) + mockGetChainInfo.mockImplementation((chainId: UniverseChainId) => { + if (chainId === UniverseChainId.Mainnet) { + return { + nativeCurrency: { name: 'Ethereum' }, + interfaceName: 'mainnet', + } as any + } + if (chainId === UniverseChainId.Polygon) { + return { + nativeCurrency: { name: 'Polygon' }, + interfaceName: 'polygon', + } as any + } + return {} as any + }) + + const result = getPossibleChainMatchFromSearchWord('polygon', enabledChains) + + expect(result).toBe(UniverseChainId.Polygon) + }) + + it('should match arbitrum interface name', () => { + const enabledChains = [UniverseChainId.Mainnet, UniverseChainId.ArbitrumOne] + + mockIsTestnetChain.mockReturnValue(false) + mockGetChainInfo.mockImplementation((chainId: UniverseChainId) => { + if (chainId === UniverseChainId.Mainnet) { + return { + nativeCurrency: { name: 'Ethereum' }, + interfaceName: 'mainnet', + } as any + } + if (chainId === UniverseChainId.ArbitrumOne) { + return { + nativeCurrency: { name: 'Ethereum' }, + interfaceName: 'arbitrum', + } as any + } + return {} as any + }) + + const result = getPossibleChainMatchFromSearchWord('arbitrum', enabledChains) + + expect(result).toBe(UniverseChainId.ArbitrumOne) + }) + }) + + describe('when handling testnet chains', () => { + it('should skip testnet chains', () => { + const enabledChains = [UniverseChainId.Mainnet, UniverseChainId.Sepolia] + + mockIsTestnetChain.mockImplementation((chainId: UniverseChainId) => chainId === UniverseChainId.Sepolia) + mockGetChainInfo.mockImplementation((chainId: UniverseChainId) => { + if (chainId === UniverseChainId.Mainnet) { + return { + nativeCurrency: { name: 'Ethereum' }, + interfaceName: 'mainnet', + } as any + } + if (chainId === UniverseChainId.Sepolia) { + return { + nativeCurrency: { name: 'Ethereum' }, + interfaceName: 'sepolia', + } as any + } + return {} as any + }) + + const result = getPossibleChainMatchFromSearchWord('ethereum', enabledChains) + + expect(result).toBe(UniverseChainId.Mainnet) + expect(mockIsTestnetChain).toHaveBeenCalledWith(UniverseChainId.Mainnet) + // The function returns early when it finds a match, so it doesn't check Sepolia + // This is the correct behavior - it should return the first non-testnet match + }) + + it('should return undefined when only testnet chains match', () => { + const enabledChains = [UniverseChainId.Sepolia] + + mockIsTestnetChain.mockReturnValue(true) + mockGetChainInfo.mockImplementation((chainId: UniverseChainId) => { + if (chainId === UniverseChainId.Sepolia) { + return { + nativeCurrency: { name: 'Ethereum' }, + interfaceName: 'sepolia', + } as any + } + return {} as any + }) + + const result = getPossibleChainMatchFromSearchWord('ethereum', enabledChains) + + expect(result).toBeUndefined() + }) + }) + + describe('when no matches are found', () => { + it('should return undefined when no chains match', () => { + const enabledChains = [UniverseChainId.Mainnet, UniverseChainId.Polygon] + + mockIsTestnetChain.mockReturnValue(false) + mockGetChainInfo.mockImplementation((chainId: UniverseChainId) => { + if (chainId === UniverseChainId.Mainnet) { + return { + nativeCurrency: { name: 'Ethereum' }, + interfaceName: 'mainnet', + } as any + } + if (chainId === UniverseChainId.Polygon) { + return { + nativeCurrency: { name: 'Polygon' }, + interfaceName: 'polygon', + } as any + } + return {} as any + }) + + const result = getPossibleChainMatchFromSearchWord('bitcoin', enabledChains) + + expect(result).toBeUndefined() + }) + + it('should return undefined when enabledChains is empty', () => { + const enabledChains: UniverseChainId[] = [] + + const result = getPossibleChainMatchFromSearchWord('ethereum', enabledChains) + + expect(result).toBeUndefined() + }) + + it('should return undefined when all chains are testnets', () => { + const enabledChains = [UniverseChainId.Sepolia, UniverseChainId.UnichainSepolia] + + mockIsTestnetChain.mockReturnValue(true) + mockGetChainInfo.mockImplementation((chainId: UniverseChainId) => { + if (chainId === UniverseChainId.Sepolia) { + return { + nativeCurrency: { name: 'Ethereum' }, + interfaceName: 'sepolia', + } as any + } + if (chainId === UniverseChainId.UnichainSepolia) { + return { + nativeCurrency: { name: 'Ethereum' }, + interfaceName: 'unichain-sepolia', + } as any + } + return {} as any + }) + + const result = getPossibleChainMatchFromSearchWord('ethereum', enabledChains) + + expect(result).toBeUndefined() + }) + }) + + describe('when multiple chains could match', () => { + it('should return the first matching chain', () => { + const enabledChains = [UniverseChainId.Mainnet, UniverseChainId.Polygon, UniverseChainId.ArbitrumOne] + + mockIsTestnetChain.mockReturnValue(false) + mockGetChainInfo.mockImplementation((chainId: UniverseChainId) => { + if (chainId === UniverseChainId.Mainnet) { + return { + nativeCurrency: { name: 'Ethereum' }, + interfaceName: 'mainnet', + } as any + } + if (chainId === UniverseChainId.Polygon) { + return { + nativeCurrency: { name: 'Ethereum' }, + interfaceName: 'polygon', + } as any + } + if (chainId === UniverseChainId.ArbitrumOne) { + return { + nativeCurrency: { name: 'Ethereum' }, + interfaceName: 'arbitrum', + } as any + } + return {} as any + }) + + const result = getPossibleChainMatchFromSearchWord('ethereum', enabledChains) + + expect(result).toBe(UniverseChainId.Mainnet) + }) + }) + + describe('edge cases', () => { + it('should handle native currency names with empty first word', () => { + const enabledChains = [UniverseChainId.Mainnet] + + mockIsTestnetChain.mockReturnValue(false) + mockGetChainInfo.mockImplementation((chainId: UniverseChainId) => { + if (chainId === UniverseChainId.Mainnet) { + return { + nativeCurrency: { name: ' Ethereum' }, // Leading space + interfaceName: 'mainnet', + } as any + } + return {} as any + }) + + const result = getPossibleChainMatchFromSearchWord('ethereum', enabledChains) + + expect(result).toBeUndefined() + }) + + it('should handle interface names with special characters', () => { + const enabledChains = [UniverseChainId.Mainnet] + + mockIsTestnetChain.mockReturnValue(false) + mockGetChainInfo.mockImplementation((chainId: UniverseChainId) => { + if (chainId === UniverseChainId.Mainnet) { + return { + nativeCurrency: { name: 'Ethereum' }, + interfaceName: 'main-net', // With hyphen + } as any + } + return {} as any + }) + + const result = getPossibleChainMatchFromSearchWord('main-net', enabledChains) + + expect(result).toBe(UniverseChainId.Mainnet) + }) + + it('should match base chain interface name', () => { + const enabledChains = [UniverseChainId.Mainnet, UniverseChainId.Base] + + mockIsTestnetChain.mockReturnValue(false) + mockGetChainInfo.mockImplementation((chainId: UniverseChainId) => { + if (chainId === UniverseChainId.Mainnet) { + return { + nativeCurrency: { name: 'Ethereum' }, + interfaceName: 'mainnet', + } as any + } + if (chainId === UniverseChainId.Base) { + return { + nativeCurrency: { name: 'Ethereum' }, + interfaceName: 'base', + } as any + } + return {} as any + }) + + const result = getPossibleChainMatchFromSearchWord('base', enabledChains) + + expect(result).toBe(UniverseChainId.Base) + }) + }) +}) diff --git a/packages/uniswap/src/utils/search/getPossibleChainMatchFromSearchWord.ts b/packages/uniswap/src/utils/search/getPossibleChainMatchFromSearchWord.ts new file mode 100644 index 00000000000..5331ee7c0a2 --- /dev/null +++ b/packages/uniswap/src/utils/search/getPossibleChainMatchFromSearchWord.ts @@ -0,0 +1,47 @@ +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { isTestnetChain } from 'uniswap/src/features/chains/utils' + +/** + * Finds a matching chain ID based on the provided chain name. + * This is intended to check if a singular word is a chain name. It doesn't + * look for chain names in multi-word searches. + * + * @param maybeChainName - The potential chain name to match against + * @param enabledChains - Array of enabled chain IDs to search within + * @returns The matching UniverseChainId or undefined if no match found + */ +export function getPossibleChainMatchFromSearchWord( + maybeChainName: string, + enabledChains: UniverseChainId[], +): UniverseChainId | undefined { + if (!maybeChainName) { + return undefined + } + + const lowerCaseChainName = maybeChainName.toLowerCase() + + for (const chainId of enabledChains) { + if (isTestnetChain(chainId)) { + continue + } + + const chainInfo = getChainInfo(chainId) + + // Check against native currency name + const nativeCurrencyName = chainInfo.nativeCurrency.name.toLowerCase() + const firstWord = nativeCurrencyName.split(' ')[0] + + if (firstWord && firstWord === lowerCaseChainName) { + return chainId + } + + // Check against interface name + const interfaceName = chainInfo.interfaceName.toLowerCase() + if (interfaceName === lowerCaseChainName) { + return chainId + } + } + + return undefined +} diff --git a/packages/uniswap/src/utils/search/parseChainFromTokenSearchQuery.test.ts b/packages/uniswap/src/utils/search/parseChainFromTokenSearchQuery.test.ts new file mode 100644 index 00000000000..cfecb304f8e --- /dev/null +++ b/packages/uniswap/src/utils/search/parseChainFromTokenSearchQuery.test.ts @@ -0,0 +1,138 @@ +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { parseChainFromTokenSearchQuery } from 'uniswap/src/utils/search/parseChainFromTokenSearchQuery' + +describe('parseChainFromTokenSearchQuery', () => { + const enabledChains: UniverseChainId[] = [ + UniverseChainId.Mainnet, + UniverseChainId.ArbitrumOne, + UniverseChainId.Base, + UniverseChainId.Optimism, + UniverseChainId.Polygon, + ] + + describe('null/empty input handling', () => { + it('returns empty result for null/empty/whitespace inputs', () => { + expect(parseChainFromTokenSearchQuery(null, enabledChains)).toEqual({ chainFilter: null, searchTerm: null }) + expect(parseChainFromTokenSearchQuery('', enabledChains)).toEqual({ chainFilter: null, searchTerm: null }) + expect(parseChainFromTokenSearchQuery(' ', enabledChains)).toEqual({ chainFilter: null, searchTerm: null }) + }) + }) + + describe('single word searches', () => { + it('returns chain filter for chain names (native currency and interface)', () => { + expect(parseChainFromTokenSearchQuery('ethereum', enabledChains)).toEqual({ + chainFilter: UniverseChainId.Mainnet, + searchTerm: null, + }) + expect(parseChainFromTokenSearchQuery('mainnet', enabledChains)).toEqual({ + chainFilter: UniverseChainId.Mainnet, + searchTerm: null, + }) + expect(parseChainFromTokenSearchQuery('EtHeReUm', enabledChains)).toEqual({ + chainFilter: UniverseChainId.Mainnet, + searchTerm: null, + }) // case insensitive + }) + + it('returns search term for non-chain words', () => { + expect(parseChainFromTokenSearchQuery('dai', enabledChains)).toEqual({ chainFilter: null, searchTerm: 'dai' }) + expect(parseChainFromTokenSearchQuery('unsupported', enabledChains)).toEqual({ + chainFilter: null, + searchTerm: 'unsupported', + }) + }) + }) + + describe('multi-word searches', () => { + it('parses chain from first word', () => { + expect(parseChainFromTokenSearchQuery('ethereum dai', enabledChains)).toEqual({ + chainFilter: UniverseChainId.Mainnet, + searchTerm: 'dai', + }) + expect(parseChainFromTokenSearchQuery('ethereum dai token', enabledChains)).toEqual({ + chainFilter: UniverseChainId.Mainnet, + searchTerm: 'dai token', + }) + expect(parseChainFromTokenSearchQuery('arbitrum uni corn token', enabledChains)).toEqual({ + chainFilter: UniverseChainId.ArbitrumOne, + searchTerm: 'uni corn token', + }) + }) + + it('parses chain from last word when first word is not a chain', () => { + expect(parseChainFromTokenSearchQuery('dai ethereum', enabledChains)).toEqual({ + chainFilter: UniverseChainId.Mainnet, + searchTerm: 'dai', + }) + expect(parseChainFromTokenSearchQuery('uni corn token base', enabledChains)).toEqual({ + chainFilter: UniverseChainId.Base, + searchTerm: 'uni corn token', + }) + }) + + it('prioritizes first word chain match over last word', () => { + expect(parseChainFromTokenSearchQuery('base token ethereum', enabledChains)).toEqual({ + chainFilter: UniverseChainId.Base, + searchTerm: 'token ethereum', + }) + expect(parseChainFromTokenSearchQuery('ethereum token base', enabledChains)).toEqual({ + chainFilter: UniverseChainId.Mainnet, + searchTerm: 'token base', + }) + }) + }) + + describe('edge cases', () => { + it('handles extra spaces and trimming', () => { + expect(parseChainFromTokenSearchQuery(' ethereum dai ', enabledChains)).toEqual({ + chainFilter: UniverseChainId.Mainnet, + searchTerm: 'dai', + }) + expect(parseChainFromTokenSearchQuery('ethereum dai', enabledChains)).toEqual({ + chainFilter: UniverseChainId.Mainnet, + searchTerm: 'dai', + }) + }) + + it('returns original search when no chain is found', () => { + expect(parseChainFromTokenSearchQuery('random search terms', enabledChains)).toEqual({ + chainFilter: null, + searchTerm: 'random search terms', + }) + }) + + it('handles chain name that matches but no search term remains', () => { + expect(parseChainFromTokenSearchQuery('ethereum', enabledChains)).toEqual({ + chainFilter: UniverseChainId.Mainnet, + searchTerm: null, + }) + }) + }) + + describe('different chain types', () => { + it('parses various chain types', () => { + expect(parseChainFromTokenSearchQuery('arbitrum dai', enabledChains)).toEqual({ + chainFilter: UniverseChainId.ArbitrumOne, + searchTerm: 'dai', + }) + expect(parseChainFromTokenSearchQuery('base usdc', enabledChains)).toEqual({ + chainFilter: UniverseChainId.Base, + searchTerm: 'usdc', + }) + expect(parseChainFromTokenSearchQuery('optimism link', enabledChains)).toEqual({ + chainFilter: UniverseChainId.Optimism, + searchTerm: 'link', + }) + expect(parseChainFromTokenSearchQuery('polygon matic', enabledChains)).toEqual({ + chainFilter: UniverseChainId.Polygon, + searchTerm: 'matic', + }) + }) + }) + + describe('empty enabled chains', () => { + it('returns search term when no chains are enabled', () => { + expect(parseChainFromTokenSearchQuery('eth dai', [])).toEqual({ chainFilter: null, searchTerm: 'eth dai' }) + }) + }) +}) diff --git a/packages/uniswap/src/utils/search/parseChainFromTokenSearchQuery.ts b/packages/uniswap/src/utils/search/parseChainFromTokenSearchQuery.ts new file mode 100644 index 00000000000..b44c3db8dad --- /dev/null +++ b/packages/uniswap/src/utils/search/parseChainFromTokenSearchQuery.ts @@ -0,0 +1,81 @@ +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { getPossibleChainMatchFromSearchWord } from 'uniswap/src/utils/search/getPossibleChainMatchFromSearchWord' + +/** + * Parses a search query to extract chain filter and search term. + * Handles patterns like "eth dai", "dai eth", "ethereum usdc", etc. + * + * @param searchQuery - The search query string + * @param enabledChains - Array of enabled chain IDs to search within + * @returns An object containing the parsed `chainFilter` and `searchTerm` + */ +export function parseChainFromTokenSearchQuery( + searchQuery: string | null, + enabledChains: UniverseChainId[], +): { + chainFilter: UniverseChainId | null + searchTerm: string | null +} { + if (!searchQuery) { + return { + chainFilter: null, + searchTerm: null, + } + } + + const sanitizedSearch = searchQuery.trim().replace(/\s+/g, ' ') + const splitSearch = sanitizedSearch.split(' ') + if (splitSearch.length === 0) { + return { + chainFilter: null, + searchTerm: null, + } + } + + if (splitSearch.length === 1) { + const singleWordSearch = splitSearch[0] + const searchChainMatch = singleWordSearch + ? getPossibleChainMatchFromSearchWord(singleWordSearch, enabledChains) + : undefined + if (searchChainMatch) { + return { + chainFilter: searchChainMatch, + searchTerm: null, + } + } else { + return { + chainFilter: null, + searchTerm: splitSearch[0] || null, + } + } + } + + const firstWord = splitSearch[0]?.toLowerCase() + const lastWord = splitSearch[splitSearch.length - 1]?.toLowerCase() + + const firstWordChainMatch = firstWord ? getPossibleChainMatchFromSearchWord(firstWord, enabledChains) : undefined + const lastWordChainMatch = lastWord ? getPossibleChainMatchFromSearchWord(lastWord, enabledChains) : undefined + + if (firstWordChainMatch) { + // First word is chain, rest is search term + const search = splitSearch.slice(1).join(' ').trim() + return { + chainFilter: firstWordChainMatch, + searchTerm: search || null, + } + } + + if (lastWordChainMatch) { + // Last word is chain, preceding words are search term + const search = splitSearch.slice(0, -1).join(' ').trim() + return { + chainFilter: lastWordChainMatch, + searchTerm: search || null, + } + } + + return { + chainFilter: null, + searchTerm: searchQuery, + } +} diff --git a/packages/uniswap/tsconfig.json b/packages/uniswap/tsconfig.json index 156cfd244ec..0e4e5fb492b 100644 --- a/packages/uniswap/tsconfig.json +++ b/packages/uniswap/tsconfig.json @@ -13,6 +13,9 @@ }, { "path": "../config" + }, + { + "path": "../gating" } ], "compilerOptions": { diff --git a/packages/wallet/package.json b/packages/wallet/package.json index 9ce21858e57..7695cf79342 100644 --- a/packages/wallet/package.json +++ b/packages/wallet/package.json @@ -29,12 +29,13 @@ "@scure/bip32": "1.3.2", "@tanstack/react-query": "5.77.2", "@uniswap/analytics-events": "2.43.0", - "@uniswap/client-data-api": "0.0.14", + "@uniswap/client-data-api": "0.0.18", "@uniswap/permit2-sdk": "1.3.0", "@uniswap/router-sdk": "2.0.2", "@uniswap/sdk-core": "7.7.2", "@uniswap/universal-router-sdk": "4.19.5", "@universe/api": "workspace:^", + "@universe/gating": "workspace:^", "apollo3-cache-persist": "0.14.1", "dayjs": "1.11.7", "ethers": "5.7.2", diff --git a/packages/wallet/src/components/landing/LandingBackground.tsx b/packages/wallet/src/components/landing/LandingBackground.tsx index 656492eb7a3..d39a33cb798 100644 --- a/packages/wallet/src/components/landing/LandingBackground.tsx +++ b/packages/wallet/src/components/landing/LandingBackground.tsx @@ -76,7 +76,7 @@ const OnboardingAnimation = ({ easing: Easing.elastic(1.1), }), ) - }, [uniswapLogoScale]) + }, []) useTimeout(() => { setShowAnimatedElements(true) @@ -184,7 +184,7 @@ const AnimatedElements = ({ ) innerAnimation.value = withDelay(INNER_CIRCLE_SHOW_DELAY, withSpring(0.8)) outerAnimation.value = withDelay(OUTER_CIRCLE_SHOW_DELAY, withSpring(0.8)) - }, [innerAnimation, outerAnimation, rotation]) + }, []) const innerCircleStyle = useAnimatedStyle(() => { return { diff --git a/packages/wallet/src/components/smartWallet/smartAccounts/hooks.ts b/packages/wallet/src/components/smartWallet/smartAccounts/hooks.ts index 5b252e271bb..72f960be889 100644 --- a/packages/wallet/src/components/smartWallet/smartAccounts/hooks.ts +++ b/packages/wallet/src/components/smartWallet/smartAccounts/hooks.ts @@ -1,8 +1,7 @@ +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { useCallback, useEffect, useState } from 'react' import { useSelector } from 'react-redux' import { AccountType } from 'uniswap/src/features/accounts/types' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { DEFAULT_TOAST_HIDE_DELAY } from 'uniswap/src/features/notifications/constants' import { useSuccessfulSwapCompleted } from 'uniswap/src/features/transactions/hooks/useSuccessfulSwapCompleted' import { TransactionDetails } from 'uniswap/src/features/transactions/types/transactionDetails' diff --git a/packages/wallet/src/features/gating/userPropertyHooks.ts b/packages/wallet/src/features/gating/userPropertyHooks.ts index 3fe58e437cc..f9f9a571080 100644 --- a/packages/wallet/src/features/gating/userPropertyHooks.ts +++ b/packages/wallet/src/features/gating/userPropertyHooks.ts @@ -1,8 +1,8 @@ +import { getStatsigClient } from '@universe/gating' import { useEffect } from 'react' import { useUnitagsAddressQuery } from 'uniswap/src/data/apiClients/unitagsApi/useUnitagsAddressQuery' import { AccountType } from 'uniswap/src/features/accounts/types' import { useENSName } from 'uniswap/src/features/ens/api' -import { getStatsigClient } from 'uniswap/src/features/gating/sdk/statsig' import { Platform } from 'uniswap/src/features/platforms/types/Platform' import { getValidAddress } from 'uniswap/src/utils/addresses' import { logger } from 'utilities/src/logger/logger' diff --git a/packages/wallet/src/features/smartWallet/hooks/useNetworkBalances.tsx b/packages/wallet/src/features/smartWallet/hooks/useNetworkBalances.tsx index ced5d4c5724..75146ef0bb6 100644 --- a/packages/wallet/src/features/smartWallet/hooks/useNetworkBalances.tsx +++ b/packages/wallet/src/features/smartWallet/hooks/useNetworkBalances.tsx @@ -1,8 +1,8 @@ +import { useStatsigClientStatus } from '@universe/gating' import { useEffect, useState } from 'react' import { fetchGasFeeQuery } from 'uniswap/src/data/apiClients/uniswapApi/useGasFeeQuery' import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' import { DEFAULT_NATIVE_ADDRESS } from 'uniswap/src/features/chains/evm/defaults' -import { useStatsigClientStatus } from 'uniswap/src/features/gating/hooks' import { createEthersProvider } from 'uniswap/src/features/providers/createEthersProvider' import { useSmartWalletChains } from 'wallet/src/features/smartWallet/hooks/useSmartWalletChains' import { NetworkInfo } from 'wallet/src/features/smartWallet/InsufficientFundsNetworkRow' diff --git a/packages/wallet/src/features/transactions/contexts/WalletUniswapContext.tsx b/packages/wallet/src/features/transactions/contexts/WalletUniswapContext.tsx index 4a3da58b3a1..3fa70472e1b 100644 --- a/packages/wallet/src/features/transactions/contexts/WalletUniswapContext.tsx +++ b/packages/wallet/src/features/transactions/contexts/WalletUniswapContext.tsx @@ -1,11 +1,10 @@ +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { ethers } from 'ethers' import React, { PropsWithChildren, useCallback, useEffect, useState } from 'react' import { UniswapProvider } from 'uniswap/src/contexts/UniswapContext' import { getDelegationService } from 'uniswap/src/domains/services' import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { UniverseChainId } from 'uniswap/src/features/chains/types' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { useUpdateDelegatedState } from 'uniswap/src/features/smartWallet/delegation/hooks/useUpdateDelegateState' import { useHasAccountMismatchCallback } from 'uniswap/src/features/smartWallet/mismatch/hooks' import { MismatchContextProvider } from 'uniswap/src/features/smartWallet/mismatch/MismatchContext' diff --git a/packages/wallet/src/features/transactions/executeTransaction/services/featureFlagService.ts b/packages/wallet/src/features/transactions/executeTransaction/services/featureFlagService.ts index ea11a986c21..2780711d0a6 100644 --- a/packages/wallet/src/features/transactions/executeTransaction/services/featureFlagService.ts +++ b/packages/wallet/src/features/transactions/executeTransaction/services/featureFlagService.ts @@ -1,5 +1,5 @@ -import { ExperimentProperties } from 'uniswap/src/features/gating/experiments' -import type { FeatureFlags } from 'uniswap/src/features/gating/flags' +import type { FeatureFlags } from '@universe/gating' +import { ExperimentProperties } from '@universe/gating' export interface FeatureFlagService { isFeatureEnabled(flagName: FeatureFlags): boolean diff --git a/packages/wallet/src/features/transactions/executeTransaction/services/featureFlagServiceImpl.ts b/packages/wallet/src/features/transactions/executeTransaction/services/featureFlagServiceImpl.ts index b55151e3c28..dd5b6ec99a5 100644 --- a/packages/wallet/src/features/transactions/executeTransaction/services/featureFlagServiceImpl.ts +++ b/packages/wallet/src/features/transactions/executeTransaction/services/featureFlagServiceImpl.ts @@ -1,6 +1,4 @@ -import { ExperimentProperties } from 'uniswap/src/features/gating/experiments' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { getExperimentValue, getFeatureFlag } from 'uniswap/src/features/gating/hooks' +import { ExperimentProperties, FeatureFlags, getExperimentValue, getFeatureFlag } from '@universe/gating' import { FeatureFlagService } from 'wallet/src/features/transactions/executeTransaction/services/featureFlagService' export const createFeatureFlagService = (): FeatureFlagService => { diff --git a/packages/wallet/src/features/transactions/executeTransaction/services/transactionConfigServiceImpl.test.ts b/packages/wallet/src/features/transactions/executeTransaction/services/transactionConfigServiceImpl.test.ts index 74c9a33abd9..fb0d18f8bfc 100644 --- a/packages/wallet/src/features/transactions/executeTransaction/services/transactionConfigServiceImpl.test.ts +++ b/packages/wallet/src/features/transactions/executeTransaction/services/transactionConfigServiceImpl.test.ts @@ -1,5 +1,5 @@ +import { FeatureFlags } from '@universe/gating' import { UniverseChainId } from 'uniswap/src/features/chains/types' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { logger as loggerUtil } from 'utilities/src/logger/logger' import { isPrivateRpcSupportedOnChain } from 'wallet/src/features/providers/utils' import { FeatureFlagService } from 'wallet/src/features/transactions/executeTransaction/services/featureFlagService' diff --git a/packages/wallet/src/features/transactions/executeTransaction/services/transactionConfigServiceImpl.ts b/packages/wallet/src/features/transactions/executeTransaction/services/transactionConfigServiceImpl.ts index 7c4f3d0a18c..97651db7513 100644 --- a/packages/wallet/src/features/transactions/executeTransaction/services/transactionConfigServiceImpl.ts +++ b/packages/wallet/src/features/transactions/executeTransaction/services/transactionConfigServiceImpl.ts @@ -1,6 +1,5 @@ +import { Experiments, FeatureFlags, PrivateRpcProperties } from '@universe/gating' import { UniverseChainId } from 'uniswap/src/features/chains/types' -import { Experiments, PrivateRpcProperties } from 'uniswap/src/features/gating/experiments' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { DEFAULT_FLASHBOTS_ENABLED } from 'uniswap/src/features/providers/FlashbotsCommon' import { logger as loggerUtil } from 'utilities/src/logger/logger' import { isPrivateRpcSupportedOnChain } from 'wallet/src/features/providers/utils' diff --git a/packages/wallet/src/features/transactions/executeTransaction/tryGetNonce.ts b/packages/wallet/src/features/transactions/executeTransaction/tryGetNonce.ts index 2b443309e17..ac9f241eb22 100644 --- a/packages/wallet/src/features/transactions/executeTransaction/tryGetNonce.ts +++ b/packages/wallet/src/features/transactions/executeTransaction/tryGetNonce.ts @@ -1,11 +1,15 @@ +import { + Experiments, + FeatureFlags, + getExperimentValue, + getFeatureFlagName, + getStatsigClient, + PrivateRpcProperties, +} from '@universe/gating' import { SagaIterator } from 'redux-saga' import { call, select } from 'typed-redux-saga' import { SignerMnemonicAccountMeta } from 'uniswap/src/features/accounts/types' import { UniverseChainId } from 'uniswap/src/features/chains/types' -import { Experiments, PrivateRpcProperties } from 'uniswap/src/features/gating/experiments' -import { FeatureFlags, getFeatureFlagName } from 'uniswap/src/features/gating/flags' -import { getExperimentValue } from 'uniswap/src/features/gating/hooks' -import { getStatsigClient } from 'uniswap/src/features/gating/sdk/statsig' import { DEFAULT_FLASHBOTS_ENABLED } from 'uniswap/src/features/providers/FlashbotsCommon' import { makeSelectAddressTransactions } from 'uniswap/src/features/transactions/selectors' import { isClassic } from 'uniswap/src/features/transactions/swap/utils/routing' diff --git a/packages/wallet/src/features/transactions/send/TokenSelectorPanel.tsx b/packages/wallet/src/features/transactions/send/TokenSelectorPanel.tsx index 5cf5a03e7a9..453caf0b2f1 100644 --- a/packages/wallet/src/features/transactions/send/TokenSelectorPanel.tsx +++ b/packages/wallet/src/features/transactions/send/TokenSelectorPanel.tsx @@ -1,5 +1,4 @@ import { Currency, CurrencyAmount } from '@uniswap/sdk-core' -import { Maybe } from 'graphql/jsutils/Maybe' import { useTranslation } from 'react-i18next' import { Flex, Text, TouchableArea } from 'ui/src' import { RotatableChevron } from 'ui/src/components/icons' diff --git a/packages/wallet/src/features/transactions/swap/confirmation.ts b/packages/wallet/src/features/transactions/swap/confirmation.ts index b0adbcde1c4..c784e69e98a 100644 --- a/packages/wallet/src/features/transactions/swap/confirmation.ts +++ b/packages/wallet/src/features/transactions/swap/confirmation.ts @@ -1,9 +1,8 @@ import { TradingApi } from '@universe/api' +import { FeatureFlags, getFeatureFlagName, getStatsigClient } from '@universe/gating' import { SagaGenerator, take } from 'typed-redux-saga' import { getDelegationService } from 'uniswap/src/domains/services' import { UniverseChainId } from 'uniswap/src/features/chains/types' -import { FeatureFlags, getFeatureFlagName } from 'uniswap/src/features/gating/flags' -import { getStatsigClient } from 'uniswap/src/features/gating/sdk/statsig' import { finalizeTransaction } from 'uniswap/src/features/transactions/slice' import { PermitMethod, SwapTxAndGasInfo } from 'uniswap/src/features/transactions/swap/types/swapTxAndGasInfo' import { TransactionStatus } from 'uniswap/src/features/transactions/types/transactionDetails' diff --git a/packages/wallet/src/features/transactions/swap/executeSwapSaga.ts b/packages/wallet/src/features/transactions/swap/executeSwapSaga.ts index 29439a7438c..10380eabbda 100644 --- a/packages/wallet/src/features/transactions/swap/executeSwapSaga.ts +++ b/packages/wallet/src/features/transactions/swap/executeSwapSaga.ts @@ -1,8 +1,7 @@ +import { DynamicConfigs, getDynamicConfigValue, SyncTransactionSubmissionChainIdsConfigKey } from '@universe/gating' import { call, put } from 'typed-redux-saga' import type { SignerMnemonicAccountMeta } from 'uniswap/src/features/accounts/types' import type { UniverseChainId } from 'uniswap/src/features/chains/types' -import { DynamicConfigs, SyncTransactionSubmissionChainIdsConfigKey } from 'uniswap/src/features/gating/configs' -import { getDynamicConfigValue } from 'uniswap/src/features/gating/hooks' import { pushNotification } from 'uniswap/src/features/notifications/slice/slice' import { AppNotificationType } from 'uniswap/src/features/notifications/slice/types' import type { SwapTradeBaseProperties } from 'uniswap/src/features/telemetry/types' diff --git a/packages/wallet/src/features/transactions/swap/hooks/useSwapHandlers.ts b/packages/wallet/src/features/transactions/swap/hooks/useSwapHandlers.ts index 420ac15b9a1..ab5987f0921 100644 --- a/packages/wallet/src/features/transactions/swap/hooks/useSwapHandlers.ts +++ b/packages/wallet/src/features/transactions/swap/hooks/useSwapHandlers.ts @@ -1,9 +1,8 @@ +import { FeatureFlags, getFeatureFlag } from '@universe/gating' import { useCallback, useMemo } from 'react' import { useDispatch, useSelector } from 'react-redux' import { AccountMeta } from 'uniswap/src/features/accounts/types' import { usePortfolioTotalValue } from 'uniswap/src/features/dataApi/balances/balancesRest' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { getFeatureFlag } from 'uniswap/src/features/gating/hooks' import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext' import { SwapEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' diff --git a/packages/wallet/src/features/transactions/swap/modals/QueuedOrderModal.tsx b/packages/wallet/src/features/transactions/swap/modals/QueuedOrderModal.tsx index 18e6819fb83..db13d952f17 100644 --- a/packages/wallet/src/features/transactions/swap/modals/QueuedOrderModal.tsx +++ b/packages/wallet/src/features/transactions/swap/modals/QueuedOrderModal.tsx @@ -1,4 +1,5 @@ import { CurrencyAmount, TradeType } from '@uniswap/sdk-core' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useDispatch } from 'react-redux' @@ -10,8 +11,6 @@ import { Modal } from 'uniswap/src/components/modals/Modal' import { LearnMoreLink } from 'uniswap/src/components/text/LearnMoreLink' import { uniswapUrls } from 'uniswap/src/constants/urls' import { AssetType, TradeableAsset } from 'uniswap/src/entities/assets' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { ModalName } from 'uniswap/src/features/telemetry/constants' import { useCurrencyInfo } from 'uniswap/src/features/tokens/useCurrencyInfo' import { diff --git a/packages/wallet/src/features/transactions/swap/prepareAndSignSwapSaga.test.ts b/packages/wallet/src/features/transactions/swap/prepareAndSignSwapSaga.test.ts index dcd61b4350c..8b9eda0d70f 100644 --- a/packages/wallet/src/features/transactions/swap/prepareAndSignSwapSaga.test.ts +++ b/packages/wallet/src/features/transactions/swap/prepareAndSignSwapSaga.test.ts @@ -35,7 +35,8 @@ jest.mock('wallet/src/features/transactions/factories/createTransactionServices' const mockPrivateRpcFlag = jest.fn().mockReturnValue(true) -jest.mock('uniswap/src/features/gating/sdk/statsig', () => ({ +jest.mock('@universe/gating', () => ({ + ...jest.requireActual('@universe/gating'), getStatsigClient: jest.fn(() => ({ checkGate: jest.fn().mockImplementation((flagName: string) => { if (flagName === 'mev-blocker') { diff --git a/packages/wallet/src/features/transactions/swap/prepareAndSignSwapSaga.ts b/packages/wallet/src/features/transactions/swap/prepareAndSignSwapSaga.ts index 86d53f2ebfe..a130e9568ae 100644 --- a/packages/wallet/src/features/transactions/swap/prepareAndSignSwapSaga.ts +++ b/packages/wallet/src/features/transactions/swap/prepareAndSignSwapSaga.ts @@ -1,7 +1,6 @@ +import { FeatureFlags, getFeatureFlagName, getStatsigClient } from '@universe/gating' import { call, select } from 'typed-redux-saga' import type { SignerMnemonicAccountMeta } from 'uniswap/src/features/accounts/types' -import { FeatureFlags, getFeatureFlagName } from 'uniswap/src/features/gating/flags' -import { getStatsigClient } from 'uniswap/src/features/gating/sdk/statsig' import type { PrepareSwapParams } from 'uniswap/src/features/transactions/swap/types/swapHandlers' import { PermitMethod } from 'uniswap/src/features/transactions/swap/types/swapTxAndGasInfo' import { isBridge, isClassic, isUniswapX, isWrap } from 'uniswap/src/features/transactions/swap/utils/routing' diff --git a/packages/wallet/src/features/transactions/swap/settings/SwapProtection.tsx b/packages/wallet/src/features/transactions/swap/settings/SwapProtection.tsx index c4a1760c11b..70a8d61e30e 100644 --- a/packages/wallet/src/features/transactions/swap/settings/SwapProtection.tsx +++ b/packages/wallet/src/features/transactions/swap/settings/SwapProtection.tsx @@ -1,9 +1,9 @@ +import { FeatureFlags } from '@universe/gating' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { useDispatch } from 'react-redux' import { Switch, Text } from 'ui/src' import { getChainLabel } from 'uniswap/src/features/chains/utils' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { Platform } from 'uniswap/src/features/platforms/types/Platform' import type { TransactionSettingConfig } from 'uniswap/src/features/transactions/components/settings/types' import { useSwapFormStoreDerivedSwapInfo } from 'uniswap/src/features/transactions/swap/stores/swapFormStore/useSwapFormStore' diff --git a/packages/wallet/src/features/transactions/swap/swapSaga.test.ts b/packages/wallet/src/features/transactions/swap/swapSaga.test.ts index cf2475ead8d..739e6bce957 100644 --- a/packages/wallet/src/features/transactions/swap/swapSaga.test.ts +++ b/packages/wallet/src/features/transactions/swap/swapSaga.test.ts @@ -42,7 +42,8 @@ import { SwapProtectionSetting } from 'wallet/src/features/wallet/slice' import { signerMnemonicAccount } from 'wallet/src/test/fixtures' import { getTxProvidersMocks } from 'wallet/src/test/mocks' -jest.mock('uniswap/src/features/gating/sdk/statsig', () => ({ +jest.mock('@universe/gating', () => ({ + ...jest.requireActual('@universe/gating'), getStatsigClient: jest.fn(() => ({ checkGate: jest.fn().mockReturnValue(true), getLayer: jest.fn(() => ({ diff --git a/packages/wallet/src/features/transactions/swap/swapSaga.ts b/packages/wallet/src/features/transactions/swap/swapSaga.ts index 6b265b5347b..378fad172d7 100644 --- a/packages/wallet/src/features/transactions/swap/swapSaga.ts +++ b/packages/wallet/src/features/transactions/swap/swapSaga.ts @@ -1,14 +1,13 @@ import { permit2Address } from '@uniswap/permit2-sdk' import { TradingApi } from '@universe/api' +import { Experiments, FeatureFlags, getFeatureFlagName, getStatsigClient } from '@universe/gating' import { call, put, select } from 'typed-redux-saga' import { SignerMnemonicAccountMeta } from 'uniswap/src/features/accounts/types' -import { FeatureFlags, getFeatureFlagName } from 'uniswap/src/features/gating/flags' -import { getStatsigClient } from 'uniswap/src/features/gating/sdk/statsig' import { pushNotification } from 'uniswap/src/features/notifications/slice/slice' import { AppNotificationType } from 'uniswap/src/features/notifications/slice/types' import { SwapTradeBaseProperties } from 'uniswap/src/features/telemetry/types' -import { FLASHBLOCKS_UI_SKIP_ROUTES } from 'uniswap/src/features/transactions/swap/components/UnichainInstantBalanceModal/constants' -import { getIsFlashblocksEnabled } from 'uniswap/src/features/transactions/swap/hooks/useIsUnichainFlashblocksEnabled' +import { logExperimentQualifyingEvent } from 'uniswap/src/features/telemetry/utils/logExperimentQualifyingEvent' +import { getFlashblocksExperimentStatus } from 'uniswap/src/features/transactions/swap/hooks/useIsUnichainFlashblocksEnabled' import { PermitMethod, ValidatedSwapTxContext } from 'uniswap/src/features/transactions/swap/types/swapTxAndGasInfo' import { isUniswapX } from 'uniswap/src/features/transactions/swap/utils/routing' import { tradeToTransactionInfo } from 'uniswap/src/features/transactions/swap/utils/trade' @@ -217,8 +216,19 @@ export function* approveAndSwap(params: SwapParams) { } yield* call(executeTransaction, executeTransactionParams) - // Only show pending notification if not a flashblock transaction - if (!getIsFlashblocksEnabled(chainId) || FLASHBLOCKS_UI_SKIP_ROUTES.includes(swapTxContext.routing)) { + const { shouldLogQualifyingEvent, shouldShowModal } = getFlashblocksExperimentStatus({ + chainId, + routing: swapTxContext.routing, + }) + + if (shouldLogQualifyingEvent) { + logExperimentQualifyingEvent({ + experiment: Experiments.UnichainFlashblocksModal, + }) + } + + // Show pending notification for control variant or ineligible swaps + if (!shouldShowModal) { yield* put(pushNotification({ type: AppNotificationType.SwapPending, wrapType: WrapType.NotApplicable })) } diff --git a/packages/wallet/src/features/transactions/watcher/transactionFinalizationSaga.ts b/packages/wallet/src/features/transactions/watcher/transactionFinalizationSaga.ts index d6090419b58..1e11344cda9 100644 --- a/packages/wallet/src/features/transactions/watcher/transactionFinalizationSaga.ts +++ b/packages/wallet/src/features/transactions/watcher/transactionFinalizationSaga.ts @@ -1,14 +1,13 @@ import { ApolloClient, NormalizedCacheObject } from '@apollo/client' import { TradeType } from '@uniswap/sdk-core' import { SharedQueryClient } from '@universe/api' +import { Experiments, getExperimentValue, PrivateRpcProperties } from '@universe/gating' import { BigNumber } from 'ethers' import { call, put, select, takeEvery } from 'typed-redux-saga' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { getChainLabel } from 'uniswap/src/features/chains/utils' import { getGasPrice } from 'uniswap/src/features/gas/types' import { findLocalGasStrategy } from 'uniswap/src/features/gas/utils' -import { Experiments, PrivateRpcProperties } from 'uniswap/src/features/gating/experiments' -import { getExperimentValue } from 'uniswap/src/features/gating/hooks' import { setNotificationStatus } from 'uniswap/src/features/notifications/slice/slice' import { Platform } from 'uniswap/src/features/platforms/types/Platform' import { refetchQueries } from 'uniswap/src/features/portfolio/portfolioUpdates/refetchQueriesSaga' diff --git a/packages/wallet/src/features/transactions/watcher/watchOnChainTransactionSaga.test.ts b/packages/wallet/src/features/transactions/watcher/watchOnChainTransactionSaga.test.ts index f2b65470cbf..46548927883 100644 --- a/packages/wallet/src/features/transactions/watcher/watchOnChainTransactionSaga.test.ts +++ b/packages/wallet/src/features/transactions/watcher/watchOnChainTransactionSaga.test.ts @@ -27,7 +27,8 @@ import { getProvider } from 'wallet/src/features/wallet/context' let mockGates: Record = {} -jest.mock('uniswap/src/features/gating/sdk/statsig', () => ({ +jest.mock('@universe/gating', () => ({ + ...jest.requireActual('@universe/gating'), getStatsigClient: jest.fn(() => ({ checkGate: jest.fn((gate: string) => mockGates[gate] ?? false), getDynamicConfig: jest.fn(() => ({ diff --git a/packages/wallet/src/features/transactions/watcher/watchOnChainTransactionSaga.ts b/packages/wallet/src/features/transactions/watcher/watchOnChainTransactionSaga.ts index 97956f77add..b134be6e2df 100644 --- a/packages/wallet/src/features/transactions/watcher/watchOnChainTransactionSaga.ts +++ b/packages/wallet/src/features/transactions/watcher/watchOnChainTransactionSaga.ts @@ -1,9 +1,8 @@ import { ApolloClient, NormalizedCacheObject } from '@apollo/client' +import { FeatureFlags, getFeatureFlagName, getStatsigClient } from '@universe/gating' import { BigNumber, BigNumberish, providers } from 'ethers' import { call, cancel, delay, fork, put, race, spawn, take } from 'typed-redux-saga' import { UniverseChainId } from 'uniswap/src/features/chains/types' -import { FeatureFlags, getFeatureFlagName } from 'uniswap/src/features/gating/flags' -import { getStatsigClient } from 'uniswap/src/features/gating/sdk/statsig' import { pushNotification } from 'uniswap/src/features/notifications/slice/slice' import { AppNotificationType } from 'uniswap/src/features/notifications/slice/types' import { waitForFlashbotsProtectReceipt } from 'uniswap/src/features/providers/FlashbotsCommon' diff --git a/packages/wallet/tsconfig.json b/packages/wallet/tsconfig.json index 96488672116..038c024dc0f 100644 --- a/packages/wallet/tsconfig.json +++ b/packages/wallet/tsconfig.json @@ -11,6 +11,9 @@ { "path": "../ui" }, + { + "path": "../gating" + }, { "path": "../api" } diff --git a/scripts/clean.sh b/scripts/clean.sh index 2f6aef5b8f6..fe3a27eb887 100644 --- a/scripts/clean.sh +++ b/scripts/clean.sh @@ -1,25 +1,78 @@ #!/bin/bash set -euo pipefail -# Restore the monorepo as close to a freshly cloned state as possible (except for the .env files) - -# Safety check - confirm with user before proceeding -echo "⚠️ WARNING: This will remove ALL untracked files and directories from your repository!" -echo "Only .env files will be preserved." -read -p "Are you sure you want to continue? (y/N): " -n 1 -r -echo -if [[ ! $REPLY =~ ^[Yy]$ ]]; then - echo "Operation cancelled." - exit 1 +# Restore the monorepo as close to a freshly cloned state as possible +# Usage: bun clean [--git] [--node] [--bun] +# --git Remove git untracked files (except for .env files, node_modules, and .claude directories) +# --node Remove all node_modules (instead of just local packages) +# --bun Clear the global bun cache + +# Parse CLI arguments +GIT_CLEAN=false +NODE_MODULES=false +BUN_CACHE=false +HAS_CLI_ARGS=false + +while [[ $# -gt 0 ]]; do + HAS_CLI_ARGS=true + case $1 in + --git) + GIT_CLEAN=true + shift + ;; + --node) + NODE_MODULES=true + shift + ;; + --bun) + BUN_CACHE=true + shift + ;; + *) + echo "Unknown argument: $1" + echo "Usage: $0 [--git] [--node] [--bun]" + exit 1 + ;; + esac +done + +prompt_yes_no() { + local message=$1 + local var_name=$2 + echo "$message" + read -p "(y/n): " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + eval "$var_name=true" + fi +} + +# Only prompt if no CLI args were provided +if [ "$HAS_CLI_ARGS" = false ]; then + prompt_yes_no "⚠️ UNTRACKED FILES: Do you want to remove all files untracked by git..." "GIT_CLEAN" + prompt_yes_no "📦 NODE MODULES: Local packages will be cleaned. Do you also want to remove ALL other node_modules (slower but more thorough)?" "NODE_MODULES" fi -# Remove all untracked files -echo "Removing all untracked files..." -git clean -fdx -e "**/.env*" +# Execute git clean if confirmed +if [ "$GIT_CLEAN" = true ]; then + echo "Removing all untracked files except for .env files..." + git clean -fdx -e "**/.env*" -e "**/node_modules" -e "**/.claude" +fi + +# Execute node_modules cleanup +if [ "$NODE_MODULES" = true ]; then + echo "Removing node_modules..." + bun run g:rm:nodemodules +else + echo "Removing local packages..." + bun run g:rm:local-packages +fi -# Remove node_modules -echo "Removing node_modules..." -bun run g:rm:nodemodules +# Clear global bun cache +if [ "$BUN_CACHE" = true ]; then + echo "Clearing global bun cache..." + bun pm cache rm +fi # Install dependencies echo "Installing dependencies..." @@ -28,6 +81,9 @@ bun install # Clear NX cache echo "Clearing NX cache..." bun nx reset +# Sync NX but silence errors because sometimes the first NX command +# after a reset fails due to a race condition with the NX daemon +bun nx sync 2>/dev/null || true # Prepare packages echo "Preparing packages..." diff --git a/scripts/remove-local-packages.sh b/scripts/remove-local-packages.sh new file mode 100755 index 00000000000..69bb087ad78 --- /dev/null +++ b/scripts/remove-local-packages.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -euo pipefail + +# This script queries NX and then removes local monorepo packages from node_modules + +projects=$(bun nx show projects) + +while IFS= read -r project; do + project_path="node_modules/$project" + echo "Removing $project_path" + rm -rf "$project_path" +done <<< "$projects" + +echo "Done removing local packages from node_modules" diff --git a/tools/uniswap-nx/src/generators/package/files/biome.json b/tools/uniswap-nx/src/generators/package/files/biome.json deleted file mode 100644 index 8e08a6604fc..00000000000 --- a/tools/uniswap-nx/src/generators/package/files/biome.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "//", - "formatter": { - "includes": ["**"] - } -} diff --git a/tools/uniswap-nx/src/generators/package/files/tsconfig.json b/tools/uniswap-nx/src/generators/package/files/tsconfig.json index 7b64995b539..194d7e1a230 100644 --- a/tools/uniswap-nx/src/generators/package/files/tsconfig.json +++ b/tools/uniswap-nx/src/generators/package/files/tsconfig.json @@ -5,7 +5,7 @@ "compilerOptions": { "noEmit": false, "emitDeclarationOnly": true, - "types": [<%= types %>], + "types": [<%- types %>], "paths": {} }, "references": [] diff --git a/tools/uniswap-nx/src/generators/package/package.ts b/tools/uniswap-nx/src/generators/package/package.ts index 56d5156071c..c926ad5b16f 100644 --- a/tools/uniswap-nx/src/generators/package/package.ts +++ b/tools/uniswap-nx/src/generators/package/package.ts @@ -1,5 +1,6 @@ import { addProjectConfiguration, generateFiles, Tree, updateJson } from '@nx/devkit' import { addTsConfigPath } from '@nx/js' +import { execSync } from 'child_process' import * as path from 'path' import { PackageGeneratorSchema } from './schema' @@ -23,14 +24,33 @@ export async function packageGenerator(tree: Tree, options: PackageGeneratorSche return json }) const relativePathToRoot = path.relative(options.path, '') + const typesList = options.types.split(',').map((t) => t.trim()) + const types = JSON.stringify(typesList).slice(1, -1) // Remove outer brackets to fit in template generateFiles(tree, path.join(__dirname, 'files'), projectRoot, { ...options, relativePathToRoot, - types: options.types - .split(',') - .map((t) => `"${t.trim()}"`) - .join(', '), + types, }) + + // Return a task that formats only the files changed by this generator + return () => { + // Get only the files that were changed by this generator + const changedFiles = tree.listChanges().map(change => change.path).join(' ') + + if (!changedFiles) { + return + } + + try { + console.log('Formatting generated files with Biome...') + // Run biome directly on just the files changed by this generator + execSync(`bun biome format --write ${changedFiles}`, { + stdio: 'inherit', + }) + } catch (error) { + console.warn('Could not format files. You may need to run "bun g:format" manually.') + } + } } export default packageGenerator diff --git a/tsconfig.base.json b/tsconfig.base.json index 61f218e91c4..32364947eab 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -40,7 +40,9 @@ "ui/*": ["packages/ui/*"], "uniswap/*": ["packages/uniswap/*"], "utilities/*": ["packages/utilities/*"], - "wallet/*": ["packages/wallet/*"] + "wallet/*": ["packages/wallet/*"], + "@universe/gating/*": ["packages/gating/*"], + "@universe/notifications/*": ["packages/notifications/*"] } } } diff --git a/tsconfig.json b/tsconfig.json index 72b73c7c20c..5b0b2b7b2f8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -36,6 +36,12 @@ }, { "path": "./tools/uniswap-nx" + }, + { + "path": "./packages/gating" + }, + { + "path": "./packages/notifications" } ] } From e3999984174a5d70f185395ce83fea8a4e5c90ba Mon Sep 17 00:00:00 2001 From: "snyk-io[bot]" <141718529+snyk-io[bot]@users.noreply.github.com> Date: Sat, 20 Dec 2025 22:12:31 +0000 Subject: [PATCH 26/50] fix: apps/mobile/Gemfile & apps/mobile/Gemfile.lock to reduce vulnerabilities The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-RUBY-AWSSDKS3-14465282 --- apps/mobile/Gemfile | 2 +- apps/mobile/Gemfile.lock | 48 ++++++++++++++++++---------------------- 2 files changed, 23 insertions(+), 27 deletions(-) diff --git a/apps/mobile/Gemfile b/apps/mobile/Gemfile index a9e494ea9a1..9ca02152c75 100644 --- a/apps/mobile/Gemfile +++ b/apps/mobile/Gemfile @@ -1,6 +1,6 @@ source "https://rubygems.org" -gem 'fastlane', '2.215.0' +gem 'fastlane', '2.215.1' # Exclude problematic versions of cocoapods and activesupport that causes build failures. gem 'cocoapods', '1.15.0' gem 'activesupport', '7.1.2' diff --git a/apps/mobile/Gemfile.lock b/apps/mobile/Gemfile.lock index 1cc3a294c88..9bf204faf39 100644 --- a/apps/mobile/Gemfile.lock +++ b/apps/mobile/Gemfile.lock @@ -1,10 +1,7 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.7) - base64 - nkf - rexml + CFPropertyList (3.0.9) activesupport (7.1.2) base64 bigdecimal @@ -15,16 +12,16 @@ GEM minitest (>= 5.1) mutex_m tzinfo (~> 2.0) - addressable (2.8.7) - public_suffix (>= 2.0.2, < 7.0) + addressable (2.8.8) + public_suffix (>= 2.0.2, < 8.0) algoliasearch (1.27.5) httpclient (~> 2.8, >= 2.8.3) json (>= 1.5.1) artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.4.0) - aws-partitions (1.1170.0) - aws-sdk-core (3.233.0) + aws-partitions (1.1198.0) + aws-sdk-core (3.240.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -32,18 +29,18 @@ GEM bigdecimal jmespath (~> 1, >= 1.6.1) logger - aws-sdk-kms (1.113.0) - aws-sdk-core (~> 3, >= 3.231.0) + aws-sdk-kms (1.118.0) + aws-sdk-core (~> 3, >= 3.239.1) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.199.1) - aws-sdk-core (~> 3, >= 3.231.0) + aws-sdk-s3 (1.208.0) + aws-sdk-core (~> 3, >= 3.234.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) aws-sigv4 (1.12.1) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) base64 (0.3.0) - bigdecimal (3.3.1) + bigdecimal (4.0.1) claide (1.1.0) cocoapods (1.15.0) addressable (~> 2.8) @@ -111,9 +108,9 @@ GEM faraday-rack (~> 1.0) faraday-retry (~> 1.0) ruby2_keywords (>= 0.0.4) - faraday-cookie_jar (0.0.7) + faraday-cookie_jar (0.0.8) faraday (>= 0.8.0) - http-cookie (~> 1.0.0) + http-cookie (>= 1.0.0) faraday-em_http (1.0.0) faraday-em_synchrony (1.0.1) faraday-excon (1.1.0) @@ -128,7 +125,7 @@ GEM faraday_middleware (1.2.1) faraday (~> 1.0) fastimage (2.4.0) - fastlane (2.215.0) + fastlane (2.215.1) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -172,7 +169,7 @@ GEM fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.87.0) + google-apis-androidpublisher_v3 (0.92.0) google-apis-core (>= 0.15.0, < 2.a) google-apis-core (0.18.0) addressable (~> 2.5, >= 2.5.1) @@ -182,11 +179,11 @@ GEM mutex_m representable (~> 3.0) retriable (>= 2.0, < 4.a) - google-apis-iamcredentials_v1 (0.24.0) + google-apis-iamcredentials_v1 (0.26.0) google-apis-core (>= 0.15.0, < 2.a) google-apis-playcustomapp_v1 (0.17.0) google-apis-core (>= 0.15.0, < 2.a) - google-apis-storage_v1 (0.57.0) + google-apis-storage_v1 (0.58.0) google-apis-core (>= 0.15.0, < 2.a) google-cloud-core (1.8.0) google-cloud-env (>= 1.0, < 3.a) @@ -195,7 +192,7 @@ GEM base64 (~> 0.2) faraday (>= 1.0, < 3.a) google-cloud-errors (1.5.0) - google-cloud-storage (1.57.0) + google-cloud-storage (1.57.1) addressable (~> 2.8) digest-crc (~> 0.4) google-apis-core (>= 0.18, < 2) @@ -205,7 +202,7 @@ GEM googleauth (~> 1.9) mini_mime (~> 1.0) google-logging-utils (0.2.0) - googleauth (1.15.0) + googleauth (1.16.0) faraday (>= 1.0, < 3.a) google-cloud-env (~> 2.2) google-logging-utils (~> 0.1) @@ -221,7 +218,7 @@ GEM i18n (1.14.7) concurrent-ruby (~> 1.0) jmespath (1.6.2) - json (2.15.1) + json (2.18.0) jwt (2.10.2) base64 logger (1.7.0) @@ -229,19 +226,18 @@ GEM mini_mime (1.1.5) minitest (5.26.0) molinillo (0.8.0) - multi_json (1.17.0) + multi_json (1.18.0) multipart-post (2.4.1) mutex_m (0.3.0) nanaimo (0.4.0) nap (1.1.0) naturally (2.3.0) netrc (0.11.0) - nkf (0.2.0) optparse (0.1.1) os (1.1.4) plist (3.7.2) public_suffix (4.0.7) - rake (13.3.0) + rake (13.3.1) representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) @@ -295,7 +291,7 @@ DEPENDENCIES activesupport (= 7.1.2) cocoapods (= 1.15.0) concurrent-ruby (= 1.3.4) - fastlane (= 2.215.0) + fastlane (= 2.215.1) xcodeproj (= 1.27.0) BUNDLED WITH From c7e7311c5ae14069956a03d707eab7abff31503f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 16 Nov 2025 04:32:10 +0000 Subject: [PATCH 27/50] build(deps-dev): bump js-yaml Bumps the npm_and_yarn group with 1 update in the / directory: [js-yaml](https://github.com/nodeca/js-yaml). Updates `js-yaml` from 4.1.0 to 4.1.1 - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 4.1.1 dependency-type: direct:development dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f42f4722421..e99f14d975d 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "i18next": "23.10.0", "i18next-parser": "8.6.0", "inquirer": "8.2.6", - "js-yaml": "4.1.0", + "js-yaml": "4.1.1", "jsonc-parser": "3.2.0", "knip": "5.50.5", "lefthook": "1.12.2", From 479fb286265549335293d828529592aaf32563a9 Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Wed, 24 Dec 2025 15:03:46 +0000 Subject: [PATCH 28/50] fix: apps/mobile/Gemfile & apps/mobile/Gemfile.lock to reduce vulnerabilities The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-RUBY-AWSSDKS3-14465282 --- apps/mobile/Gemfile | 2 +- apps/mobile/Gemfile.lock | 48 ++++++++++++++++++---------------------- 2 files changed, 23 insertions(+), 27 deletions(-) diff --git a/apps/mobile/Gemfile b/apps/mobile/Gemfile index a9e494ea9a1..9ca02152c75 100644 --- a/apps/mobile/Gemfile +++ b/apps/mobile/Gemfile @@ -1,6 +1,6 @@ source "https://rubygems.org" -gem 'fastlane', '2.215.0' +gem 'fastlane', '2.215.1' # Exclude problematic versions of cocoapods and activesupport that causes build failures. gem 'cocoapods', '1.15.0' gem 'activesupport', '7.1.2' diff --git a/apps/mobile/Gemfile.lock b/apps/mobile/Gemfile.lock index 1cc3a294c88..0de72b95649 100644 --- a/apps/mobile/Gemfile.lock +++ b/apps/mobile/Gemfile.lock @@ -1,10 +1,7 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.7) - base64 - nkf - rexml + CFPropertyList (3.0.9) activesupport (7.1.2) base64 bigdecimal @@ -15,16 +12,16 @@ GEM minitest (>= 5.1) mutex_m tzinfo (~> 2.0) - addressable (2.8.7) - public_suffix (>= 2.0.2, < 7.0) + addressable (2.8.8) + public_suffix (>= 2.0.2, < 8.0) algoliasearch (1.27.5) httpclient (~> 2.8, >= 2.8.3) json (>= 1.5.1) artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.4.0) - aws-partitions (1.1170.0) - aws-sdk-core (3.233.0) + aws-partitions (1.1198.0) + aws-sdk-core (3.240.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -32,18 +29,18 @@ GEM bigdecimal jmespath (~> 1, >= 1.6.1) logger - aws-sdk-kms (1.113.0) - aws-sdk-core (~> 3, >= 3.231.0) + aws-sdk-kms (1.118.0) + aws-sdk-core (~> 3, >= 3.239.1) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.199.1) - aws-sdk-core (~> 3, >= 3.231.0) + aws-sdk-s3 (1.209.0) + aws-sdk-core (~> 3, >= 3.234.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) aws-sigv4 (1.12.1) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) base64 (0.3.0) - bigdecimal (3.3.1) + bigdecimal (4.0.1) claide (1.1.0) cocoapods (1.15.0) addressable (~> 2.8) @@ -111,9 +108,9 @@ GEM faraday-rack (~> 1.0) faraday-retry (~> 1.0) ruby2_keywords (>= 0.0.4) - faraday-cookie_jar (0.0.7) + faraday-cookie_jar (0.0.8) faraday (>= 0.8.0) - http-cookie (~> 1.0.0) + http-cookie (>= 1.0.0) faraday-em_http (1.0.0) faraday-em_synchrony (1.0.1) faraday-excon (1.1.0) @@ -128,7 +125,7 @@ GEM faraday_middleware (1.2.1) faraday (~> 1.0) fastimage (2.4.0) - fastlane (2.215.0) + fastlane (2.215.1) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -172,7 +169,7 @@ GEM fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.87.0) + google-apis-androidpublisher_v3 (0.93.0) google-apis-core (>= 0.15.0, < 2.a) google-apis-core (0.18.0) addressable (~> 2.5, >= 2.5.1) @@ -182,11 +179,11 @@ GEM mutex_m representable (~> 3.0) retriable (>= 2.0, < 4.a) - google-apis-iamcredentials_v1 (0.24.0) + google-apis-iamcredentials_v1 (0.26.0) google-apis-core (>= 0.15.0, < 2.a) google-apis-playcustomapp_v1 (0.17.0) google-apis-core (>= 0.15.0, < 2.a) - google-apis-storage_v1 (0.57.0) + google-apis-storage_v1 (0.58.0) google-apis-core (>= 0.15.0, < 2.a) google-cloud-core (1.8.0) google-cloud-env (>= 1.0, < 3.a) @@ -195,7 +192,7 @@ GEM base64 (~> 0.2) faraday (>= 1.0, < 3.a) google-cloud-errors (1.5.0) - google-cloud-storage (1.57.0) + google-cloud-storage (1.57.1) addressable (~> 2.8) digest-crc (~> 0.4) google-apis-core (>= 0.18, < 2) @@ -205,7 +202,7 @@ GEM googleauth (~> 1.9) mini_mime (~> 1.0) google-logging-utils (0.2.0) - googleauth (1.15.0) + googleauth (1.16.0) faraday (>= 1.0, < 3.a) google-cloud-env (~> 2.2) google-logging-utils (~> 0.1) @@ -221,7 +218,7 @@ GEM i18n (1.14.7) concurrent-ruby (~> 1.0) jmespath (1.6.2) - json (2.15.1) + json (2.18.0) jwt (2.10.2) base64 logger (1.7.0) @@ -229,19 +226,18 @@ GEM mini_mime (1.1.5) minitest (5.26.0) molinillo (0.8.0) - multi_json (1.17.0) + multi_json (1.18.0) multipart-post (2.4.1) mutex_m (0.3.0) nanaimo (0.4.0) nap (1.1.0) naturally (2.3.0) netrc (0.11.0) - nkf (0.2.0) optparse (0.1.1) os (1.1.4) plist (3.7.2) public_suffix (4.0.7) - rake (13.3.0) + rake (13.3.1) representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) @@ -295,7 +291,7 @@ DEPENDENCIES activesupport (= 7.1.2) cocoapods (= 1.15.0) concurrent-ruby (= 1.3.4) - fastlane (= 2.215.0) + fastlane (= 2.215.1) xcodeproj (= 1.27.0) BUNDLED WITH From 398296f7d53e031dbb784d6a256d9555b7735200 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 12:44:24 +0000 Subject: [PATCH 29/50] build(deps): bump the npm_and_yarn group across 3 directories with 4 updates Bumps the npm_and_yarn group with 1 update in the /apps/extension directory: [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router). Bumps the npm_and_yarn group with 4 updates in the /apps/web directory: [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router), [hono](https://github.com/honojs/hono), [qs](https://github.com/ljharb/qs) and [storybook](https://github.com/storybookjs/storybook/tree/HEAD/code/core). Bumps the npm_and_yarn group with 2 updates in the /packages/uniswap directory: [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router) and [qs](https://github.com/ljharb/qs). Updates `react-router` from 7.6.3 to 7.12.0 - [Release notes](https://github.com/remix-run/react-router/releases) - [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md) - [Commits](https://github.com/remix-run/react-router/commits/react-router@7.12.0/packages/react-router) Updates `react-router` from 7.6.3 to 7.12.0 - [Release notes](https://github.com/remix-run/react-router/releases) - [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md) - [Commits](https://github.com/remix-run/react-router/commits/react-router@7.12.0/packages/react-router) Updates `hono` from 4.10.3 to 4.11.4 - [Release notes](https://github.com/honojs/hono/releases) - [Commits](https://github.com/honojs/hono/compare/v4.10.3...v4.11.4) Updates `qs` from 6.11.0 to 6.14.1 - [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md) - [Commits](https://github.com/ljharb/qs/compare/v6.11.0...v6.14.1) Updates `storybook` from 8.5.2 to 8.6.15 - [Release notes](https://github.com/storybookjs/storybook/releases) - [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md) - [Commits](https://github.com/storybookjs/storybook/commits/v8.6.15/code/core) Updates `react-router` from 7.6.3 to 7.12.0 - [Release notes](https://github.com/remix-run/react-router/releases) - [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md) - [Commits](https://github.com/remix-run/react-router/commits/react-router@7.12.0/packages/react-router) Updates `qs` from 6.11.0 to 6.14.1 - [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md) - [Commits](https://github.com/ljharb/qs/compare/v6.11.0...v6.14.1) --- updated-dependencies: - dependency-name: react-router dependency-version: 7.12.0 dependency-type: direct:production dependency-group: npm_and_yarn - dependency-name: react-router dependency-version: 7.12.0 dependency-type: direct:production dependency-group: npm_and_yarn - dependency-name: hono dependency-version: 4.11.4 dependency-type: direct:production dependency-group: npm_and_yarn - dependency-name: qs dependency-version: 6.14.1 dependency-type: direct:production dependency-group: npm_and_yarn - dependency-name: storybook dependency-version: 8.6.15 dependency-type: direct:development dependency-group: npm_and_yarn - dependency-name: react-router dependency-version: 7.12.0 dependency-type: direct:production dependency-group: npm_and_yarn - dependency-name: qs dependency-version: 6.14.1 dependency-type: direct:production dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] --- apps/extension/package.json | 2 +- apps/web/package.json | 8 ++++---- packages/uniswap/package.json | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/extension/package.json b/apps/extension/package.json index 02974e473d0..bfd7f0ff2fd 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -41,7 +41,7 @@ "react-native-web": "0.19.13", "react-qr-code": "2.0.12", "react-redux": "8.0.5", - "react-router": "7.6.3", + "react-router": "7.12.0", "redux": "4.2.1", "redux-logger": "3.0.6", "redux-persist": "6.0.0", diff --git a/apps/web/package.json b/apps/web/package.json index b06f96e8d51..59eaf8abdbb 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -132,7 +132,7 @@ "resize-observer-polyfill": "1.5.1", "source-map-explorer": "2.5.3", "start-server-and-test": "2.0.0", - "storybook": "8.5.2", + "storybook": "8.6.15", "storybook-addon-pseudo-states": "4.0.2", "swc-loader": "0.2.6", "terser": "5.24.0", @@ -222,7 +222,7 @@ "fancy-canvas": "2.1.0", "framer-motion": "10.17.6", "graphql": "16.8.1", - "hono": "4.10.3", + "hono": "4.11.4", "html-entities": "2.6.0", "i18next": "23.10.0", "jotai": "1.3.7", @@ -239,7 +239,7 @@ "polished": "3.3.2", "polyfill-object.fromentries": "1.0.1", "porto": "0.0.80", - "qs": "6.11.0", + "qs": "6.14.1", "react": "18.3.1", "react-dom": "18.3.1", "react-feather": "2.0.10", @@ -250,7 +250,7 @@ "react-native-reanimated": "3.16.7", "react-popper": "2.3.0", "react-redux": "8.0.5", - "react-router": "7.6.3", + "react-router": "7.12.0", "react-scroll-sync": "0.11.2", "react-virtualized-auto-sizer": "1.0.20", "react-window": "1.8.9", diff --git a/packages/uniswap/package.json b/packages/uniswap/package.json index 9d4bae0b472..cae4f05b97a 100644 --- a/packages/uniswap/package.json +++ b/packages/uniswap/package.json @@ -82,7 +82,7 @@ "lodash": "4.17.21", "ms": "2.1.3", "poisson-disk-sampling": "2.3.1", - "qs": "6.11.0", + "qs": "6.14.1", "react": "18.3.1", "react-i18next": "14.1.0", "react-infinite-scroll-component": "6.1.0", @@ -98,7 +98,7 @@ "react-native-svg": "15.11.2", "react-native-webview": "13.13.5", "react-redux": "8.0.5", - "react-router": "7.6.3", + "react-router": "7.12.0", "react-test-renderer": "18.3.1", "react-virtualized-auto-sizer": "1.0.20", "react-window": "1.8.9", From 6ad0d16e6ae6498541b20414a8d4a6de60b7dffc Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Tue, 20 Jan 2026 00:05:38 +0700 Subject: [PATCH 30/50] Potential fix for code scanning alert no. 9: Incomplete regular expression for hostnames (#100) Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- apps/web/cypress/support/commands.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/cypress/support/commands.ts b/apps/web/cypress/support/commands.ts index 476905a0daa..f35f18e9b4b 100644 --- a/apps/web/cypress/support/commands.ts +++ b/apps/web/cypress/support/commands.ts @@ -144,7 +144,7 @@ export function registerCommands() { Cypress.Commands.add('interceptGraphqlOperation', (operationName, fixturePath) => { const graphqlInterceptions = Cypress.env('graphqlInterceptions') - cy.intercept(/(?:interface|beta).gateway.uniswap.org\/v1\/graphql/, (req) => { + cy.intercept(/(?:interface|beta)\.gateway\.uniswap\.org\/v1\/graphql/, (req) => { req.headers['origin'] = 'https://app.uniswap.org' const currentOperationName = req.body.operationName From 1609591f4cfb3b2fd4f010f86eb9be6ee90e058d Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Tue, 20 Jan 2026 00:17:06 +0700 Subject: [PATCH 31/50] Potential fix for code scanning alert no. 21: Incomplete URL substring sanitization Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --- .../turnstileSolver.integration.test.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/sessions/src/challenge-solvers/turnstileSolver.integration.test.ts b/packages/sessions/src/challenge-solvers/turnstileSolver.integration.test.ts index 424e096648a..f2ef7413cf8 100644 --- a/packages/sessions/src/challenge-solvers/turnstileSolver.integration.test.ts +++ b/packages/sessions/src/challenge-solvers/turnstileSolver.integration.test.ts @@ -25,7 +25,16 @@ beforeAll(() => { }) vi.spyOn(document.head, 'appendChild').mockImplementation((node) => { - if (node instanceof HTMLScriptElement && node.src.includes('challenges.cloudflare.com')) { + let isTurnstileScript = false + if (node instanceof HTMLScriptElement && node.src) { + try { + const url = new URL(node.src, window.location.href) + isTurnstileScript = url.hostname === 'challenges.cloudflare.com' + } catch { + isTurnstileScript = false + } + } + if (isTurnstileScript) { // Simulate script load immediately setTimeout(() => { // Set up the mock turnstile API From f6b397f75d76f3bbfce1219501cc4bc9f8092663 Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Tue, 20 Jan 2026 00:22:38 +0700 Subject: [PATCH 32/50] Potential fix for code scanning alert no. 22: Incomplete URL substring sanitization Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --- .../turnstileSolver.integration.test.ts | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/sessions/src/challenge-solvers/turnstileSolver.integration.test.ts b/packages/sessions/src/challenge-solvers/turnstileSolver.integration.test.ts index f2ef7413cf8..f44c0c3151b 100644 --- a/packages/sessions/src/challenge-solvers/turnstileSolver.integration.test.ts +++ b/packages/sessions/src/challenge-solvers/turnstileSolver.integration.test.ts @@ -252,12 +252,20 @@ describe('Turnstile Solver Integration Tests', () => { it('handles script loading failures', async () => { // Mock script loading failure vi.spyOn(document.head, 'appendChild').mockImplementationOnce((node) => { - if (node instanceof HTMLScriptElement && node.src.includes('challenges.cloudflare.com')) { - setTimeout(() => { - if (node.onerror) { - node.onerror({} as Event) - } - }, 0) + if (node instanceof HTMLScriptElement) { + let host: string | null = null + try { + host = new URL(node.src, window.location.href).host + } catch { + host = null + } + if (host === 'challenges.cloudflare.com') { + setTimeout(() => { + if (node.onerror) { + node.onerror({} as Event) + } + }, 0) + } } return node }) From 2aea2941360ed51ee5d52bff220bfb9f52098bec Mon Sep 17 00:00:00 2001 From: Dargon789 <64915515+Dargon789@users.noreply.github.com> Date: Tue, 20 Jan 2026 05:01:33 +0700 Subject: [PATCH 33/50] Uniswap/main (#98) * Create config.yml (#72) Summary by Sourcery CI: Introduce CircleCI 2.1 pipeline with a docker-based say-hello job and workflow Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> * ci(release): publish latest release * ci(release): publish latest release * ci(release): publish latest release * ci(release): publish latest release * ci(release): publish latest release * ci(release): publish latest release * ci(release): publish latest release * ci(release): publish latest release * ci(release): publish latest release * ci(release): publish latest release * ci(release): publish latest release * ci(release): publish latest release * ci(release): publish latest release * ci(release): publish latest release * ci(release): publish latest release * ci(release): publish latest release * ci(release): publish latest release * ci(release): publish latest release * ci(release): publish latest release * ci(release): publish latest release * ci(release): publish latest release * ci(release): publish latest release * ci(release): publish latest release * ci(release): publish latest release * ci(release): publish latest release * ci(release): publish latest release * ci(release): publish latest release * ci(release): publish latest release * ci(release): publish latest release * ci(release): publish latest release * ci(release): publish latest release * ci(release): publish latest release * ci(release): publish latest release * Potential fix for code scanning alert no. 24: Incomplete multi-character sanitization Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 23: Incomplete regular expression for hostnames Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 21: Incomplete URL substring sanitization Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> * Potential fix for code scanning alert no. 22: Incomplete URL substring sanitization Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> --------- Signed-off-by: AU_gdev_19 <64915515+Dargon789@users.noreply.github.com> Signed-off-by: Dargon789 <64915515+Dargon789@users.noreply.github.com> Co-authored-by: Uniswap Labs Service Account Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .bun-version | 2 +- .claude/hooks/skill-activation-prompt.sh | 6 + .claude/hooks/skill-activation-prompt.ts | 132 + .claude/settings.json | 32 + .claude/skills/skill-rules.json | 29 + .claude/skills/web-e2e/SKILL.md | 350 + .cursor/cli.json | 21 + .cursorignore | 7 + .env.defaults | 7 + .gitignore | 11 +- .nxignore | 1 + CLAUDE.md | 3 +- CODEOWNERS | 1 - RELEASE | 126 +- apps/api-self-serve/.eslintrc.js | 44 + apps/api-self-serve/.gitignore | 7 + apps/api-self-serve/README.md | 1 + apps/api-self-serve/app/app.css | 170 + apps/api-self-serve/app/lib/utils.ts | 6 + apps/api-self-serve/app/root.tsx | 53 + apps/api-self-serve/app/routes.ts | 3 + apps/api-self-serve/app/routes/home.tsx | 11 + apps/api-self-serve/app/welcome/logo-dark.svg | 23 + .../api-self-serve/app/welcome/logo-light.svg | 23 + apps/api-self-serve/app/welcome/welcome.tsx | 80 + apps/api-self-serve/components.json | 22 + apps/api-self-serve/package.json | 36 + apps/api-self-serve/react-router.config.ts | 7 + apps/api-self-serve/tailwind.config.ts | 430 + apps/api-self-serve/tsconfig.eslint.json | 5 + apps/api-self-serve/tsconfig.json | 23 + apps/api-self-serve/vite.config.ts | 15 + apps/cli/.eslintrc.cjs | 45 + apps/cli/README.md | 2 + apps/cli/package.json | 38 + apps/cli/project.json | 19 + apps/cli/src/cli-ui.tsx | 27 + apps/cli/src/cli.ts | 429 + apps/cli/src/core/data-collector.ts | 1096 ++ apps/cli/src/core/orchestrator.ts | 810 ++ apps/cli/src/index.ts | 2 + apps/cli/src/lib/ai-provider-vercel.ts | 143 + apps/cli/src/lib/ai-provider.ts | 50 + apps/cli/src/lib/analysis-writer.ts | 241 + apps/cli/src/lib/cache-keys.ts | 106 + apps/cli/src/lib/cache-provider-sqlite.ts | 130 + apps/cli/src/lib/cache-provider.ts | 41 + apps/cli/src/lib/logger.ts | 221 + apps/cli/src/lib/pr-body-cleaner.ts | 496 + apps/cli/src/lib/release-scanner.ts | 283 + apps/cli/src/lib/stream-handler.ts | 35 + apps/cli/src/lib/team-members.ts | 37 + apps/cli/src/lib/team-resolver.ts | 156 + apps/cli/src/lib/trivial-files.ts | 74 + apps/cli/src/prompts/bug-bisect.md | 83 + apps/cli/src/prompts/release-changelog.md | 74 + apps/cli/src/prompts/team-digest.md | 102 + apps/cli/src/ui/App.tsx | 175 + apps/cli/src/ui/components/Banner.tsx | 30 + apps/cli/src/ui/components/Box.tsx | 18 + .../src/ui/components/ChangelogPreview.tsx | 20 + apps/cli/src/ui/components/FormField.tsx | 26 + apps/cli/src/ui/components/NumberInput.tsx | 39 + .../src/ui/components/ProgressIndicator.tsx | 75 + apps/cli/src/ui/components/ReleaseList.tsx | 34 + apps/cli/src/ui/components/Select.tsx | 38 + apps/cli/src/ui/components/StatusBadge.tsx | 20 + apps/cli/src/ui/components/TextInput.tsx | 38 + apps/cli/src/ui/components/Toggle.tsx | 22 + apps/cli/src/ui/components/WindowedSelect.tsx | 101 + apps/cli/src/ui/hooks/useAnalysis.ts | 53 + apps/cli/src/ui/hooks/useAppState.tsx | 132 + apps/cli/src/ui/hooks/useEditableField.ts | 107 + apps/cli/src/ui/hooks/useFormNavigation.ts | 67 + apps/cli/src/ui/hooks/useReleases.ts | 106 + apps/cli/src/ui/hooks/useRepository.ts | 53 + apps/cli/src/ui/hooks/useTeams.ts | 94 + apps/cli/src/ui/hooks/useToggleGroup.ts | 70 + .../src/ui/screens/BugBisectResultsScreen.tsx | 464 + apps/cli/src/ui/screens/BugInputScreen.tsx | 76 + apps/cli/src/ui/screens/ConfigReview.tsx | 975 ++ apps/cli/src/ui/screens/ExecutionScreen.tsx | 176 + apps/cli/src/ui/screens/ReleaseSelector.tsx | 200 + apps/cli/src/ui/screens/ResultsScreen.tsx | 258 + apps/cli/src/ui/screens/TeamDetailsScreen.tsx | 152 + .../cli/src/ui/screens/TeamSelectorScreen.tsx | 327 + apps/cli/src/ui/screens/WelcomeScreen.tsx | 81 + .../src/ui/services/orchestrator-service.ts | 68 + apps/cli/src/ui/utils/colors.ts | 13 + apps/cli/src/ui/utils/format.ts | 18 + apps/cli/tsconfig.json | 11 + apps/cli/tsconfig.lint.json | 8 + apps/extension/.gitignore | 2 + apps/extension/jest-setup.js | 84 +- apps/extension/jest.config.js | 5 +- .../app/components/AutoLockProvider.test.tsx | 425 +- .../src/app/components/tabs/ActivityTab.tsx | 24 +- .../src/app/core/BaseAppContainer.tsx | 70 +- apps/extension/src/app/core/SidebarApp.tsx | 14 + .../src/app/features/accounts/AccountItem.tsx | 14 +- .../accounts/AccountSwitcherScreen.tsx | 26 +- .../AccountSwitcherScreen.test.tsx.snap | 323 +- .../biometricUnlock/BiometricUnlockStorage.ts | 1 + .../biometricUnlock/biometricAuthUtils.ts | 7 +- .../useBiometricUnlockSetupMutation.test.ts | 9 +- .../useBiometricUnlockSetupMutation.ts | 10 +- ...hangePasswordWithBiometricMutation.test.ts | 4 + .../useChangePasswordWithBiometricMutation.ts | 2 + ...ockWithBiometricCredentialMutation.test.ts | 44 +- ...seUnlockWithBiometricCredentialMutation.ts | 4 +- .../dappRequests/DappRequestContent.tsx | 108 +- .../usePrepareAndSignSendCallsTransaction.ts | 1 + .../Connection/ConnectionRequestContent.tsx | 34 +- .../requestContent/EthSend/EthSend.tsx | 159 +- .../ParsedTransactionRequestContent.tsx | 74 + .../EthSend/Swap/SwapRequestContent.tsx | 2 +- .../PersonalSignRequestContent.tsx | 113 +- .../SendCalls/SendCallsRequestContent.tsx | 79 +- .../NonStandardTypedDataRequestContent.tsx | 51 +- .../Permit2/Permit2RequestContent.tsx | 104 - .../src/app/features/dappRequests/saga.ts | 4 +- .../dappRequests/types/EthersTypes.ts | 2 +- .../src/app/features/home/PortfolioHeader.tsx | 2 +- .../app/features/home/TokenBalanceList.tsx | 40 +- .../home/introCards/HomeIntroCardStack.tsx | 26 +- .../onboarding/import/ImportMnemonic.tsx | 4 +- .../onboarding/import/SelectWallets.tsx | 4 +- .../app/features/onboarding/scan/OTPInput.tsx | 2 +- .../__snapshots__/ReceiveScreen.test.tsx.snap | 55 +- .../src/app/features/send/SendFlow.tsx | 26 +- .../features/settings/DeviceAccessScreen.tsx | 2 + .../features/settings/SettingsDropdown.tsx | 2 +- .../SettingsManageConnectionsScreen.tsx | 1 + .../app/features/settings/SettingsScreen.tsx | 2 +- .../password/ChangePasswordForm.test.tsx | 166 + .../settings/password/ChangePasswordForm.tsx | 45 +- .../password/CreateNewPasswordModal.tsx | 4 +- .../ChangePasswordForm.test.tsx.snap | 287 + .../password/usePasswordResetFlow.test.ts | 93 + .../settings/password/usePasswordResetFlow.ts | 6 + .../src/app/navigation/navigation.tsx | 2 +- apps/extension/src/app/saga.ts | 19 - apps/extension/src/app/utils/analytics.ts | 7 + .../src/background/backgroundDappRequests.ts | 5 +- .../background/utils/persistedStateUtils.ts | 6 + apps/extension/src/entrypoints/background.ts | 56 +- apps/extension/src/manifest.json | 2 +- apps/extension/src/store/migrations.test.ts | 32 +- apps/extension/src/store/migrations.ts | 6 +- apps/extension/src/store/schema.ts | 8 +- apps/extension/src/test/babel.config.js | 3 +- apps/extension/src/test/fixtures/redux.ts | 11 +- apps/extension/tsconfig.json | 6 + apps/extension/webpack.config.js | 5 +- apps/extension/wxt.config.ts | 12 +- apps/mobile/.depcheckrc | 5 +- apps/mobile/.eslintrc.js | 7 - apps/mobile/.fingerprintignore | 49 + apps/mobile/.gitignore | 11 +- .../flows/explore/filters-and-sorts.yaml | 22 +- apps/mobile/android/app/build.gradle | 111 +- .../android/app/src/main/AndroidManifest.xml | 70 +- .../src/main/java/com/uniswap/MainActivity.kt | 6 +- .../main/java/com/uniswap/MainApplication.kt | 15 +- .../main/java/com/uniswap/UniswapPackage.kt | 2 + .../SilentPushEventEmitterModule.kt | 129 + .../SilentPushNotificationServiceExtension.kt | 123 + .../uniswap/utils/JsonWritableExtensions.kt | 61 + apps/mobile/android/build.gradle | 25 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- apps/mobile/android/gradlew | 301 +- apps/mobile/android/settings.gradle | 39 +- apps/mobile/app.config.ts | 18 + apps/mobile/app.json | 14 - apps/mobile/eas.json | 72 + apps/mobile/fingerprint.config.js | 8 + apps/mobile/index.js | 4 +- apps/mobile/ios/Podfile | 20 +- apps/mobile/ios/Podfile.lock | 1342 +- .../ios/Uniswap.xcodeproj/project.pbxproj | 1148 +- apps/mobile/ios/Uniswap/AppDelegate.h | 7 - apps/mobile/ios/Uniswap/AppDelegate.m | 131 - apps/mobile/ios/Uniswap/AppDelegate.swift | 163 + .../Notifications/SilentPushEventEmitter.m | 18 + .../SilentPushEventEmitter.swift | 31 + .../ios/Uniswap/Uniswap-Bridging-Header.h | 23 + apps/mobile/ios/Uniswap/main.m | 9 - .../ios/WidgetsCore/.mobileschema_fingerprint | 2 +- apps/mobile/ios/sourcemaps-datadog.sh | 64 +- apps/mobile/jest-setup.js | 40 +- apps/mobile/jest.config.js | 1 - apps/mobile/metro.config.js | 65 +- apps/mobile/project.json | 60 +- apps/mobile/rnef.config.mjs | 36 - apps/mobile/scripts/check-android-gradle.sh | 25 + apps/mobile/scripts/check-podfile.sh | 25 + apps/mobile/scripts/checkBundleSize.sh | 2 +- .../scripts/getFingerprintForRadonIDE.ts | 22 + .../scripts/ios-build-interactive/main.ts | 8 +- apps/mobile/src/app/migrations.test.ts | 32 +- apps/mobile/src/app/migrations.ts | 9 +- .../src/app/modals/AccountSwitcherModal.tsx | 52 +- .../app/modals/BridgedAssetWarningWrapper.tsx | 2 +- .../app/modals/TokenWarningModalWrapper.tsx | 7 +- .../AccountSwitcherModal.test.tsx.snap | 405 +- apps/mobile/src/app/monitoredSagas.ts | 19 - .../app/navigation/NavigationContainer.tsx | 2 +- apps/mobile/src/app/navigation/constants.ts | 2 + apps/mobile/src/app/navigation/navigation.tsx | 4 + .../tabs/CustomTabBar/CustomTabBar.tsx | 4 +- .../navigation/tabs/CustomTabBar/constants.ts | 2 +- .../src/app/navigation/tabs/SwapButton.tsx | 2 +- ...ressModal.tsx => SwapLongPressOverlay.tsx} | 134 +- apps/mobile/src/app/navigation/types.ts | 4 + apps/mobile/src/app/schema.ts | 14 +- .../PriceExplorer/usePriceHistory.test.ts | 108 +- .../ConnectedDapps/DappConnectionItem.tsx | 4 +- .../ModalWithOverlay/ModalWithOverlay.tsx | 3 +- .../Requests/RequestModal/ClientDetails.tsx | 32 +- .../Requests/RequestModal/HeaderText.tsx | 10 +- .../Requests/RequestModal/RequestDetails.tsx | 3 +- .../WalletConnectRequestModal.tsx | 67 +- .../WalletConnectRequestModalContent.tsx | 274 +- .../ScanSheet/PendingConnectionModal.tsx | 132 +- .../src/components/Requests/Uwulink/utils.ts | 1 + .../PrivateKeySpeedBumpModal.test.tsx.snap | 276 +- .../EditLabelSettingsModal.tsx | 4 +- .../TokenBalanceList/TokenBalanceList.tsx | 7 + .../TokenDetailsActionButtons.tsx | 6 +- .../TokenDetails/TokenDetailsContext.tsx | 16 + .../TokenDetails/TokenDetailsLinks.tsx | 2 +- .../TokenDetails/TokenDetailsStats.tsx | 26 +- .../TokenSelector/TokenFiatOnRampList.tsx | 6 +- .../components/accounts/AccountCardItem.tsx | 5 +- .../AccountCardItem.test.tsx.snap | 181 +- .../__snapshots__/AccountHeader.test.tsx.snap | 350 +- .../__snapshots__/AccountList.test.tsx.snap | 91 +- .../__snapshots__/BackButton.test.tsx.snap | 116 +- .../__snapshots__/CloseButton.test.tsx.snap | 54 +- .../src/components/education/SeedPhrase.tsx | 69 +- .../ExploreSections/ExploreSections.tsx | 55 +- .../ExploreSections/NetworkPillsRow.tsx | 5 +- .../FavoriteHeaderRow.test.tsx.snap | 131 +- .../FavoriteTokenCard.test.tsx.snap | 237 +- .../FavoriteWalletCard.test.tsx.snap | 273 +- .../__snapshots__/RemoveButton.test.tsx.snap | 78 +- .../__snapshots__/SortButton.test.tsx.snap | 121 +- .../__snapshots__/TokenItem.test.tsx.snap | 126 +- .../explore/hooks/useFlatListAutoScroll.ts | 4 +- .../SearchPopularNFTCollections.graphql | 21 - .../src/components/fiatOnRamp/CtaButton.tsx | 2 +- .../mobile/src/components/icons/TripleDot.tsx | 17 - .../SelectionCircle.test.tsx.snap | 35 +- .../src/components/layout/TabHelpers.tsx | 2 +- .../layout/screens/HeaderScrollScreen.tsx | 8 +- .../layout/screens/ScrollHeader.tsx | 10 +- .../ReactNavigationModal.tsx | 6 + .../ReportTokenDataModalScreen.tsx | 10 + .../ReportTokenIssueModalScreen.tsx | 10 + .../src/components/text/LongMarkdownText.tsx | 2 +- .../__snapshots__/AnimatedText.test.tsx.snap | 40 +- .../__snapshots__/DecimalNumber.test.tsx.snap | 35 +- .../TextWithFuseMatches.test.tsx.snap | 161 +- .../biometrics/biometrics-utils.test.ts | 4 +- .../src/features/biometrics/biometricsSaga.ts | 2 +- .../deepLinking/deepLinkUtils.test.ts | 92 +- .../src/features/deepLinking/deepLinkUtils.ts | 97 +- .../deepLinking/handleDeepLinkSaga.test.ts | 370 - .../deepLinking/handleDeepLinkSaga.ts | 11 +- .../deepLinking/handleInAppBrowserSaga.ts | 35 - .../deepLinking/parseSwapLink.test.ts | 19 +- .../externalProfile/ProfileContextMenu.tsx | 11 +- .../externalProfile/ProfileHeader.tsx | 2 +- .../fiatOnRamp/FiatOnRampAmountSection.tsx | 2 +- .../features/import/InputWIthSuffixProps.ts | 2 +- .../GenericImportForm.test.tsx.snap | 170 +- .../collection/NFTCollectionContextMenu.tsx | 6 +- .../CollectionPreviewCard.test.tsx.snap | 135 +- .../item/__snapshots__/traits.test.tsx.snap | 7 +- .../src/features/notifications/Onesignal.ts | 9 + .../notifications/SilentPushListener.ts | 70 + .../src/features/notifications/constants.ts | 1 + .../mobile/src/features/notifications/saga.ts | 15 +- apps/mobile/src/features/send/SendFlow.tsx | 12 +- .../src/features/send/SendFormButton.tsx | 2 +- .../src/features/send/SendTokenForm.tsx | 10 +- apps/mobile/src/features/telemetry/saga.ts | 53 +- .../walletConnect/fetchDappDetails.ts | 4 +- .../src/features/walletConnect/saga.test.ts | 89 +- .../walletConnect/signWcRequestSaga.ts | 10 +- .../src/features/walletConnect/utils.test.ts | 85 + .../src/features/walletConnect/utils.ts | 62 +- .../walletConnect/walletConnectSlice.ts | 24 +- apps/mobile/src/screens/DevScreen.tsx | 2 +- apps/mobile/src/screens/ExploreScreen.tsx | 43 +- apps/mobile/src/screens/FiatOnRampScreen.tsx | 3 +- .../src/screens/Import/SelectWalletScreen.tsx | 4 +- ...oreCloudBackupPasswordScreen.test.tsx.snap | 327 +- .../RestoreCloudBackupScreen.test.tsx.snap | 531 +- .../screens/Onboarding/ManualBackupScreen.tsx | 23 +- .../src/screens/Onboarding/TermsOfService.tsx | 2 + .../__snapshots__/BackupScreen.test.tsx.snap | 778 +- ...ttingsCloudBackupPasswordConfirmScreen.tsx | 1 + apps/mobile/src/screens/SettingsScreen.tsx | 2 +- .../src/screens/TokenDetailsHeaders.tsx | 22 + .../mobile/src/screens/TokenDetailsScreen.tsx | 171 +- apps/mobile/src/test/fixtures/redux.ts | 9 +- apps/mobile/src/utils/hooks.ts | 2 +- apps/mobile/tsconfig.json | 6 + apps/web/.depcheckrc | 1 + apps/web/.env.staging | 1 + apps/web/.gitignore | 2 + apps/web/.storybook/__mocks__/tty.js | 10 + apps/web/.storybook/main.ts | 177 +- apps/web/CLAUDE.md | 14 +- apps/web/functions/api/image/pools.tsx | 6 +- .../pools/__snapshots__/pool.test.ts.snap | 8 +- apps/web/functions/explore/pools/pool.test.ts | 2 +- apps/web/playwright.config.ts | 17 +- apps/web/public/app-sitemap.xml | 12 + apps/web/public/csp.json | 5 +- .../notifications/monad_banner_light.png | Bin 0 -> 29178 bytes .../notifications/monad_logo_filled.png | Bin 0 -> 2017 bytes apps/web/public/nfts-sitemap.xml | 310 +- apps/web/public/pools-sitemap.xml | 9751 ++++--------- apps/web/public/sitemap.xml | 3 - .../{vercel-csp.json => staging-csp.json} | 3 +- apps/web/public/tokens-sitemap.xml | 9165 +------------ apps/web/scripts/start-anvil.sh | 14 + apps/web/src/appGraphql/data/apollo/client.ts | 4 +- .../src/appGraphql/data/apollo/retryLink.ts | 43 + .../appGraphql/data/pools/usePoolData.test.ts | 266 + .../src/appGraphql/data/pools/usePoolData.ts | 5 +- .../dark.svg | 22 + .../light.svg | 22 + .../dark.svg | 778 ++ .../light.svg | 778 ++ .../mobile-dark.svg | 189 + .../mobile-light.svg | 189 + .../web/src/assets/svg/demo-wallet-emblem.svg | 3 + .../AccountDetails/AddressDisplay.tsx | 4 +- .../MultiBlockchainAddressDisplay.tsx | 24 +- .../AccountDrawer/AccountDrawer.e2e.test.ts | 283 +- .../components/AccountDrawer/ActionTile.tsx | 26 +- .../AuthenticatedHeader.anvil.e2e.test.ts | 40 +- .../AuthenticatedHeader.e2e.test.ts | 40 +- .../AccountDrawer/AuthenticatedHeader.tsx | 93 +- .../AccountDrawer/DisconnectButton.tsx | 29 +- .../AccountDrawer/LocalCurrencyMenu.tsx | 4 +- .../Activity/ActivityTab.anvil.e2e.test.ts | 50 - .../MiniPortfolio/Activity/ActivityTab.tsx | 35 +- .../Activity/CancelOrdersDialog.tsx | 55 +- .../MiniPortfolio/Activity/Logos.tsx | 19 +- .../Activity/OffchainActivityModal.tsx | 16 +- .../CancelOrdersDialog.test.tsx.snap | 644 +- .../OffchainActivityModal.test.tsx.snap | 297 +- .../OffchainOrderLineItem.test.tsx.snap | 37 +- .../MiniPortfolio/Activity/hooks.ts | 11 +- .../MiniPortfolio/Activity/parseLocal.ts | 1 + .../MiniPortfolio/ExtensionDeeplinks.tsx | 11 +- .../LimitDetailActivityRow.test.tsx.snap | 6 +- .../OpenLimitOrdersButton.test.tsx.snap | 35 +- .../MiniPortfolio/MiniPortfolio.tsx | 7 +- .../MiniPortfolio/MiniPortfolioV2.tsx | 84 + .../__snapshots__/EmptyPools.test.tsx.snap | 3 +- .../MiniPortfolio/Pools/cache.ts | 2 +- .../MiniPortfolio/Tokens/TokensTab.tsx | 14 + .../AccountDrawer/MiniPortfolio/shared.tsx | 10 +- .../AccountDrawer/PortfolioBalanceMenu.tsx | 8 +- .../AccountDrawer/ReportedActivityToggle.tsx | 24 + .../src/components/AccountDrawer/Scrim.tsx | 4 +- .../components/AccountDrawer/SettingsMenu.tsx | 10 +- .../AccountDrawer/SettingsToggle.tsx | 3 +- .../AccountDrawer/SmallBalanceToggle.tsx | 1 + .../{SpamToggle.tsx => SpamTokensToggle.tsx} | 12 +- .../src/components/AccountDrawer/shared.tsx | 33 +- .../ActionTileWithIconAnimation.tsx | 43 + .../components/ActionTiles/BuyActionTile.tsx | 30 + .../components/ActionTiles/MoreActionTile.tsx | 102 + .../ActionTiles/ReceiveActionTile.tsx | 23 + .../SendActionTile/SendActionTile.tsx | 42 + .../SendActionTile}/SendButtonTooltip.tsx | 7 +- .../components/ActionTiles/SwapActionTile.tsx | 30 + apps/web/src/components/AddressInputPanel.tsx | 16 +- .../Banner/LimitedSupportBanner.tsx | 7 +- .../components/Banner/Outage/OutageBanner.tsx | 25 +- .../Banner/SolanaPromo/SolanaPromoBanner.tsx | 140 - .../Banner/SolanaPromo/SolanaPromoModal.tsx | 145 - .../shared/{Banners.tsx => OutageBanners.tsx} | 0 .../components/ChainConnectivityWarning.tsx | 16 +- .../ActiveLiquidityChart/TickTooltip.tsx | 29 +- .../web/src/components/Charts/ChartHeader.tsx | 18 +- apps/web/src/components/Charts/ChartModel.tsx | 181 +- .../src/components/Charts/ChartTooltip.tsx | 24 + .../components/Charts/CustomHoverMarker.tsx | 50 + .../components/D3LiquidityMinMaxInput.tsx | 4 +- .../components/DefaultPriceStrategies.tsx | 3 +- .../calculateAnchoredLiquidityByTick.test.ts | 223 + .../utils/calculateAnchoredLiquidityByTick.ts | 69 + .../utils/calculateTokensLocked.ts | 53 + .../LiquidityChart/utils/getAmounts.test.ts | 208 + .../Charts/LiquidityChart/utils/getAmounts.ts | 91 + .../Charts/LiquidityRangeInput/hooks.ts | 40 +- .../src/components/Charts/LiveDotRenderer.tsx | 161 + .../src/components/Charts/LoadingState.tsx | 36 +- .../Charts/StackedLineChart/index.tsx | 45 +- .../web/src/components/Charts/StaleBanner.tsx | 33 + .../Charts/ToucanChart/renderer.tsx | 413 + .../ToucanChart/toucan-chart-series.tsx | 56 + .../VolumeChart/CustomVolumeChartModel.tsx | 6 +- .../components/Charts/VolumeChart/index.tsx | 15 +- .../hooks/useApplyChartTextureEffects.ts | 90 + .../useHeaderDateFormatter.ts} | 0 apps/web/src/components/Charts/utils.tsx | 10 - .../src/components/ConfirmSwapModal/Modal.tsx | 4 +- .../components/ConfirmSwapModal/Pending.tsx | 10 +- .../ConfirmSwapModal/ProgressIndicator.tsx | 16 +- .../src/components/ConfirmSwapModal/Step.tsx | 8 +- .../ConfirmSwapModal/TradeSummary.tsx | 6 +- .../__snapshots__/Error.test.tsx.snap | 132 +- .../__snapshots__/Head.test.tsx.snap | 18 +- .../__snapshots__/Pending.test.tsx.snap | 526 +- .../src/components/ConfirmSwapModal/index.tsx | 4 +- .../components/ConnectedAccountBlocked.tsx | 10 +- .../LimitPriceInputPanel/LimitPriceButton.tsx | 6 +- .../LimitPriceInputLabel.tsx | 6 +- .../LimitPriceInputPanel.test.tsx | 10 +- .../LimitPriceInputPanel.tsx | 10 +- .../LimitPriceButton.test.tsx.snap | 242 +- .../SwapCurrencyInputPanel.test.tsx | 161 + .../SwapCurrencyInputPanel.tsx | 28 +- .../web/src/components/Dropdowns/Dropdown.tsx | 5 +- .../components/Dropdowns/DropdownSelector.tsx | 2 +- .../src/components/Dropdowns/FilterButton.tsx | 1 + apps/web/src/components/ErrorBoundary.tsx | 6 +- .../__snapshots__/index.test.tsx.snap | 4 +- apps/web/src/components/FeeSelector/index.tsx | 6 +- .../src/components/HelpModal/HelpModal.tsx | 2 + .../components/Icons/AlertTriangleFilled.tsx | 6 +- apps/web/src/components/Icons/CreditCard.tsx | 12 - apps/web/src/components/Icons/Globe.tsx | 18 - .../src/components/Icons/LoadingSpinner.tsx | 22 +- apps/web/src/components/Icons/shared.tsx | 6 +- apps/web/src/components/InputStepCounter.tsx | 8 +- .../components/Liquidity/Create/AddHook.tsx | 30 +- .../Create/DynamicFeeTierSpeedbump.tsx | 41 +- .../components/Liquidity/Create/EditStep.tsx | 3 +- .../Liquidity/Create/SelectTokenStep.tsx | 103 +- .../hooks/useDefaultInitialPrice.test.ts | 4 + .../hooks/useDerivedPositionInfo.test.ts | 8 +- .../Create/hooks/useDerivedPositionInfo.tsx | 36 +- .../Create/hooks/useLiquidityUrlState.test.ts | 8 +- .../Create/hooks/useLiquidityUrlState.ts | 5 +- .../Liquidity/FeeTierSearchModal.tsx | 16 +- .../src/components/Liquidity/HookModal.tsx | 15 +- .../LPIncentives/LpIncentiveClaimModal.tsx | 114 +- .../LPIncentives/LpIncentiveRewardsCard.tsx | 3 +- .../hooks/useLpIncentiveClaimMutation.ts | 73 - .../Liquidity/LiquidityPositionInfoBadges.tsx | 3 +- .../Liquidity/PositionPageActionButtons.tsx | 2 + .../src/components/Liquidity/ReviewModal.tsx | 334 + .../hooks/useAllFeeTierPoolData.test.tsx | 18 +- .../Liquidity/hooks/useAllFeeTierPoolData.ts | 5 +- .../hooks/useReportPositionHandler.ts | 63 + .../Liquidity/parsers/urlParsers.ts | 5 +- apps/web/src/components/Liquidity/types.ts | 2 +- .../Liquidity/utils/feeTiers.test.ts | 109 +- .../components/Liquidity/utils/feeTiers.ts | 174 +- .../Liquidity/utils/parseFromRest.test.ts | 4 +- .../Liquidity/utils/parseFromRest.ts | 4 +- apps/web/src/components/Loader/styled.tsx | 10 +- apps/web/src/components/Logo/ChainLogo.tsx | 5 +- apps/web/src/components/Logo/DoubleLogo.tsx | 4 +- .../__snapshots__/DoubleLogo.test.tsx.snap | 9 - .../NavBar/DownloadApp/Modal/DownloadApps.tsx | 2 +- .../DownloadApp/Modal/GetStarted.e2e.test.ts | 54 +- .../NewUserCTAButton.test.tsx.snap | 3 +- .../NavBar/LegalAndPrivacyMenu/index.tsx | 2 +- .../NavBar/MobileBottomBar/TDPActionTabs.tsx | 19 +- .../NavBar/PreferencesMenu/Preferences.tsx | 14 +- .../NavBar/SearchBar/SearchBar.e2e.test.ts | 56 +- .../NavBar/SearchBar/SearchModal.tsx | 6 +- .../__snapshots__/SearchBar.test.tsx.snap | 19 +- apps/web/src/components/NavBar/Tabs/Tabs.tsx | 45 +- .../NavBar/TestnetMode/TestnetModeTooltip.tsx | 3 +- .../components/NavBar/UniswapWrappedEntry.tsx | 80 + apps/web/src/components/NavBar/index.tsx | 4 +- .../NetworkFilter/NetworkFilter.tsx | 76 +- .../NetworkFilter/useFilteredChains.ts | 12 +- apps/web/src/components/NumericalInput.tsx | 11 +- .../Pools/PoolDetails/ChartSection/index.tsx | 85 +- .../PoolDetails/PoolDetailsHeader.test.tsx | 5 +- .../PoolDetails/PoolDetailsLink.test.tsx | 5 +- .../Pools/PoolDetails/PoolDetailsLink.tsx | 26 +- .../PoolDetails/PoolDetailsStats.test.tsx | 7 +- .../Pools/PoolDetails/PoolDetailsStats.tsx | 40 +- .../PoolDetailsStatsButtons.test.tsx | 15 +- .../PoolDetails/PoolDetailsStatsButtons.tsx | 19 +- .../Pools/PoolDetails/PoolDetailsTable.tsx | 4 +- .../PoolDetailsTransactionsTable.tsx | 6 +- .../PoolDetailsHeader.test.tsx.snap | 134 +- .../PoolDetailsLink.test.tsx.snap | 150 +- .../PoolDetailsStats.test.tsx.snap | 146 +- .../PoolDetailsStatsButtons.test.tsx.snap | 38 +- .../PoolDetailsTransactionTable.test.tsx.snap | 7484 +++++----- .../components/Pools/PoolDetails/shared.ts | 4 +- .../components/Pools/PoolTable/PoolTable.tsx | 31 +- .../__snapshots__/PoolTable.test.tsx.snap | 11445 ++++++++-------- apps/web/src/components/Popover.tsx | 8 +- .../components/Popups/MismatchToastItem.tsx | 2 +- apps/web/src/components/Popups/PopupItem.tsx | 20 + .../components/Popups/ToastRegularSimple.tsx | 2 +- apps/web/src/components/Popups/constants.ts | 4 + apps/web/src/components/Popups/types.ts | 10 + apps/web/src/components/PrivacyPolicy.tsx | 10 +- .../ReceiveCryptoModal/AccountOption.tsx | 6 +- .../ChooseMultiPlatformProvider.tsx | 12 +- .../RouterLabel/UniswapXRouterLabel.tsx | 4 +- .../UniswapXRouterLabel.test.tsx.snap | 4 +- apps/web/src/components/RouterLabel/index.tsx | 2 +- .../SearchModal/CurrencyList/index.tsx | 6 +- .../web/src/components/SearchModal/styled.tsx | 8 +- .../__snapshots__/index.test.tsx.snap | 78 +- apps/web/src/components/StatusIcon/index.tsx | 14 +- apps/web/src/components/SwapBottomCard.tsx | 3 +- apps/web/src/components/Table/ErrorBox.tsx | 12 +- apps/web/src/components/Table/Filter.tsx | 10 +- apps/web/src/components/Table/TableBody.tsx | 93 + apps/web/src/components/Table/TableRow.tsx | 106 + .../Table/__snapshots__/styled.test.tsx.snap | 69 +- apps/web/src/components/Table/constants.ts | 2 + apps/web/src/components/Table/icons.tsx | 33 - apps/web/src/components/Table/styled.tsx | 38 +- apps/web/src/components/Table/types.ts | 13 + apps/web/src/components/Table/utils.ts | 39 +- apps/web/src/components/Table/utils/hasRow.ts | 8 + .../web/src/components/Toggle/MultiToggle.tsx | 6 +- .../components/Tokens/TokenDetails/About.tsx | 6 +- .../ChartSection/AdvancedPriceChartToggle.tsx | 16 +- .../ChartSection/ChartTypeSelector.tsx | 91 - .../ChartSection/ChartTypeToggle.tsx | 60 + .../Tokens/TokenDetails/ChartSection/hooks.ts | 65 +- .../TokenDetails/ChartSection/index.tsx | 88 +- .../Tokens/TokenDetails/MoreButton.tsx | 48 + .../Tokens/TokenDetails/ShareButton.tsx | 29 +- .../Tokens/TokenDetails/Skeleton.tsx | 2 +- .../Tokens/TokenDetails/StatsSection.tsx | 29 +- .../Tokens/TokenDetails/TokenDescription.tsx | 125 +- .../TokenDetails/TokenDetails.e2e.test.ts | 103 +- .../TokenDetails/TokenDetailsHeader.tsx | 237 +- .../TokenDetails/TokenDetailsSwap.e2e.test.ts | 148 +- .../__snapshots__/Skeleton.test.tsx.snap | 56 +- .../TokenDescription.test.tsx.snap | 372 +- .../components/Tokens/TokenDetails/index.tsx | 92 +- .../components/Tokens/TokenDetails/shared.ts | 20 +- .../tables/TokenDetailsPoolsTable.tsx | 4 +- .../TokenDetails/tables/TransactionsTable.tsx | 4 +- .../TokenDetailsPoolsTable.test.tsx.snap | 10251 +++++++------- .../TokenTable/VolumeTimeFrameSelector.tsx | 7 +- .../components/Tokens/TokenTable/index.tsx | 41 +- apps/web/src/components/Tooltip.tsx | 4 +- .../UniswapWrapped2025Banner.tsx | 32 + .../src/components/TopLevelModals/index.tsx | 20 + .../TopLevelModals/modalRegistry.test.tsx | 6 +- .../TopLevelModals/modalRegistry.tsx | 48 +- .../src/components/TopLevelModals/types.ts | 2 +- .../Auction/AuctionStats/AuctionStats.tsx | 293 + .../Auction/BidActivities/BidActivities.tsx | 42 + .../Auction/BidActivities/BidActivity.tsx | 59 + .../BidDistributionChart.tsx | 99 +- .../BidDistributionChartFooter.tsx | 55 + .../BidDistributionChartHeader.tsx | 99 +- .../BidDistributionChartPlaceholder.tsx | 50 + .../BidDistributionChartRenderer.tsx | 770 ++ .../BlockUpdateCountdown.tsx | 23 + .../Auction/BidDistributionChart/constants.ts | 92 + .../dev/CustomizePresetForm.tsx | 472 + .../dev/MockDataSelectorModal.tsx | 139 + .../dev/SavedPresetsList.tsx | 166 + .../BidDistributionChart/dev/customPresets.ts | 270 + .../BidDistributionChart/dev/devUtils.ts | 59 + .../dev/useCustomPresetsStore.ts | 131 + .../hooks/useChartDimensions.ts | 27 + .../hooks/useChartLabels.ts | 127 + .../hooks/useChartTooltip.ts | 72 + .../utils/bidConcentration.ts | 102 + .../utils/clearingPrice/label.ts | 61 + .../utils/clearingPrice/position.test.ts | 292 + .../utils/clearingPrice/position.ts | 143 + .../BidDistributionChart/utils/utils.ts | 494 + .../src/components/Toucan/Auction/BidForm.tsx | 71 +- .../Auction/hooks/useAuctionBlockPolling.ts | 55 + .../Toucan/Auction/hooks/useBidTokenInfo.ts | 63 + .../store/AuctionStoreContextProvider.tsx | 8 + .../Auction/store/createAuctionStore.ts | 39 +- .../Toucan/Auction/store/mockData.ts | 87 +- .../store/mocks/distributionData/100_Ticks.ts | 111 + .../store/mocks/distributionData/10_Ticks.ts | 21 + .../store/mocks/distributionData/20_Ticks.ts | 31 + .../store/mocks/distributionData/50_Ticks.ts | 62 + .../bidDistributionMockData.ts | 15 + .../Auction/store/mocks/useMockDataStore.ts | 97 + .../components/Toucan/Auction/store/types.ts | 50 +- .../Auction/utils/computeAuctionProgress.ts | 76 + .../Toucan/Shared/ToucanContainer.tsx | 4 +- .../components/Toucan/TopAuctionsTable.tsx | 118 + apps/web/src/components/V2Unsupported.tsx | 4 +- .../WalletModal/PrivacyPolicyNotice.tsx | 4 +- .../WalletModal/UniswapWalletOptions.tsx | 2 +- .../UniswapWalletOptions.test.tsx.snap | 5 +- apps/web/src/components/WalletOneLinkQR.tsx | 6 +- .../Web3Provider/TestWeb3Provider.tsx | 1 - .../Web3Provider/WebUniswapContext.tsx | 21 +- .../Web3Provider/createWeb3Provider.tsx | 21 +- .../components/Web3Provider/walletConnect.ts | 2 +- apps/web/src/components/Web3Status/index.tsx | 15 +- .../Web3Status/useShowPendingAfterDelay.ts | 43 +- apps/web/src/components/animations/Wiggle.tsx | 75 +- .../delegation/DelegationMismatchModal.tsx | 47 +- apps/web/src/components/deprecated/Column.tsx | 8 +- apps/web/src/components/deprecated/Row.tsx | 10 +- apps/web/src/components/earn/styled.tsx | 12 +- .../emptyWallet/EmptyWalletCards.tsx | 46 +- .../src/components/swap/DetailLineItem.tsx | 6 +- .../components/swap/GasBreakdownTooltip.tsx | 6 +- .../components/swap/GasEstimateTooltip.tsx | 4 +- apps/web/src/components/swap/SwapDetails.tsx | 10 +- .../components/swap/SwapModalHeaderAmount.tsx | 4 +- apps/web/src/components/swap/SwapPreview.tsx | 4 +- apps/web/src/components/swap/SwapSkeleton.tsx | 24 +- apps/web/src/components/swap/TradePrice.tsx | 4 +- .../__snapshots__/SwapDetails.test.tsx.snap | 32 +- .../__snapshots__/SwapLineItem.test.tsx.snap | 322 +- .../__snapshots__/SwapPreview.test.tsx.snap | 245 +- .../__snapshots__/SwapSkeleton.test.tsx.snap | 32 +- apps/web/src/components/swap/styled.tsx | 18 +- apps/web/src/constants/icons/mxnIcon.tsx | 1 + .../src/featureFlags/flags/outageBanner.ts | 49 - .../WebNotificationToastWrapper.tsx | 3 + apps/web/src/hooks/useBlockCountdown.test.ts | 76 + apps/web/src/hooks/useBlockCountdown.ts | 55 + apps/web/src/hooks/useBlockTimestamp.ts | 2 +- apps/web/src/hooks/useChainOutageConfig.ts | 20 + apps/web/src/hooks/useColor.ts | 13 +- apps/web/src/hooks/useContract.ts | 6 +- apps/web/src/hooks/useERC20Permit.ts | 12 +- .../hooks/useHandleUniswapXActivityUpdate.tsx | 7 +- apps/web/src/hooks/useIsPage.ts | 4 + apps/web/src/hooks/useLpIncentivesClaim.ts | 26 - .../web/src/hooks/useLpIncentivesClaimData.ts | 99 +- apps/web/src/hooks/useOnClickOutside.ts | 6 +- apps/web/src/hooks/usePoolTickData.ts | 2 +- apps/web/src/hooks/usePrevious.ts | 2 +- apps/web/src/hooks/useScroll.ts | 59 +- apps/web/src/hooks/useSelectChain.ts | 90 +- apps/web/src/hooks/useUnmountingAnimation.ts | 4 +- .../src/hooks/useUpdateManualOutage.test.tsx | 271 + apps/web/src/hooks/useUpdateManualOutage.ts | 73 + .../src/hooks/useV2LiquidityTokenPermit.ts | 4 +- apps/web/src/lib/styled-components.ts | 31 +- apps/web/src/lib/utils/analytics.ts | 6 + .../WebNotificationService.tsx | 218 + .../createLocalStorageAdapter.ts | 91 + ...eateLegacyBannersNotificationDataSource.ts | 368 + .../ModalNotification.tsx | 104 + .../NotificationContainer.tsx | 127 + .../StackedLowerLeftBanners.tsx | 89 + .../createWebNotificationRenderer.ts | 43 + .../notificationStore.ts | 36 + .../stackingUtils.test.ts | 94 + .../notification-renderer/stackingUtils.ts | 27 + .../telemetry/getNotificationTelemetry.ts | 36 + apps/web/src/pages/App.smoketest.e2e.test.ts | 22 +- apps/web/src/pages/App/AppBody.tsx | 4 +- apps/web/src/pages/App/Header.tsx | 12 +- apps/web/src/pages/App/Layout.tsx | 6 +- .../pages/App/utils/UserPropertyUpdater.tsx | 25 + .../CreateLiquidityContextProvider.tsx | 24 +- .../pages/CreatePosition/CreatePosition.tsx | 2 +- .../CreatePositionModal.test.tsx | 336 + .../CreatePositionTxContext.test.ts | 23 +- apps/web/src/pages/Errors.e2e.test.ts | 44 +- .../web/src/pages/Explore/Explore.e2e.test.ts | 30 +- .../src/pages/Explore/ExploreStatsSection.tsx | 9 +- .../pages/Explore/TokenExplore.e2e.test.ts | 122 +- .../Explore/TokenExploreFilter.e2e.test.ts | 54 +- apps/web/src/pages/Explore/Toucan.tsx | 18 - .../src/pages/Explore/ToucanToken/index.tsx | 13 + apps/web/src/pages/Explore/constants.ts | 2 +- apps/web/src/pages/Explore/index.tsx | 28 +- apps/web/src/pages/Explore/redirects.tsx | 14 +- .../Explore/tables/RecentTransactions.tsx | 26 +- .../IncreaseLiquidity.anvil.e2e.test.ts | 128 +- .../IncreaseLiquidityReview.tsx | 94 +- .../IncreaseLiquidityTxContext.tsx | 39 +- .../pages/Landing/assets/approvedTokens.ts | 37 +- .../src/pages/Landing/components/StatCard.tsx | 33 +- .../Landing/components/TokenCloud/Ticker.tsx | 2 +- .../pages/Landing/components/animations.tsx | 6 +- .../components/cards/DownloadWalletCard.tsx | 23 +- .../components/cards/TradingApiCard.tsx | 4 +- .../components/cards/ValuePropCard.tsx | 6 +- .../Landing/components/cards/WebappCard.tsx | 14 +- apps/web/src/pages/Landing/sections/Stats.tsx | 11 +- .../src/pages/Landing/sections/useInView.tsx | 2 +- .../Migrate/MigrateLiquidityTxContext.tsx | 39 + .../pages/Migrate/MigrateV3.anvil.e2e.test.ts | 91 + .../src/pages/Migrate/MigrateV3.e2e.test.ts | 39 + .../hooks/useInitialPosition.ts | 0 .../hooks/useMigrateLPPositionTxInfo.ts | 453 + .../pages/{MigrateV3 => Migrate}/index.tsx | 141 +- .../web/src/pages/MigrateV2/MigrateV2Pair.tsx | 1 + .../pages/MigrateV2/Settings/Input/index.tsx | 6 +- .../Settings/MaxSlippageSettings/index.tsx | 6 +- .../Settings/MultipleRoutingOptions.tsx | 4 +- .../RouterPreferenceSettings/index.tsx | 4 +- .../MigrateV3/MigrateV3.anvil.e2e.test.ts | 81 - .../src/pages/MigrateV3/MigrateV3.e2e.test.ts | 29 - .../MigrateV3/MigrateV3LiquidityTxContext.tsx | 307 - apps/web/src/pages/NavBar.e2e.test.ts | 286 +- apps/web/src/pages/NotFound/index.tsx | 10 +- .../pages/PoolDetails/PoolDetails.e2e.test.ts | 40 +- apps/web/src/pages/PoolDetails/index.test.tsx | 5 +- apps/web/src/pages/PoolDetails/index.tsx | 44 +- .../Portfolio/Activity/ActivityFilters.tsx | 82 + .../ActivityTable/ActivityAddressCell.tsx | 82 + .../ActivityAddressLookupStore.tsx | 125 + .../ActivityAmountCell/ActivityAmountCell.tsx | 348 + .../ActivityAmountCell/ApproveAmountCell.tsx | 68 + .../ActivityAmountCell/CompactLayout.tsx | 25 + .../ActivityAmountCell/DualTokenLayout.tsx | 87 + .../ActivityAmountCell/EmptyCell.tsx | 9 + .../ActivityAmountCell/utils.tsx | 104 + .../Activity/ActivityTable/ActivityTable.tsx | 186 + .../ActivityTable/AddressWithAvatar.tsx | 46 + .../ActivityTable/NftAmountDisplay.tsx | 57 + .../Activity/ActivityTable/TimeCell.tsx | 59 + .../ActivityTable/TokenAmountDisplay.tsx | 34 + .../ActivityTable/TransactionTypeCell.tsx | 52 + .../ActivityTable/activityTableModels.ts | 81 + .../Activity/ActivityTable/registry.ts | 366 + .../Activity/PaginationSkeletonRow.tsx | 31 + .../ConnectWalletFixedBottomButton.tsx | 132 + apps/web/src/pages/Portfolio/Defi.tsx | 2 +- .../src/pages/Portfolio/EmptyTableCell.tsx | 9 + apps/web/src/pages/Portfolio/Header/Tabs.tsx | 77 +- .../Header/hooks/usePortfolioParams.ts | 31 - .../Header/hooks/usePortfolioRoutes.ts | 35 + .../Header/hooks/usePortfolioTabs.ts | 6 +- .../Header/useShouldHeaderBeCompact.tsx | 26 + .../pages/Portfolio/NFTs/NFTCardSkeleton.tsx | 22 + .../Portfolio/NFTs/generateRotationStyle.ts | 30 + apps/web/src/pages/Portfolio/Overview.tsx | 89 - .../pages/Portfolio/Overview/ActionTiles.tsx | 61 + .../Portfolio/Overview/MiniActivityTable.tsx | 211 + .../MiniPoolsTable/MiniPoolsTable.tsx | 214 + .../MiniPoolsTable/columns/Balance.tsx | 33 + .../Overview/MiniPoolsTable/columns/Fees.tsx | 45 + .../Overview/MiniPoolsTable/columns/Info.tsx | 63 + .../MiniPoolsTable/columns/Status.tsx | 59 + .../Portfolio/Overview/MiniTokensTable.tsx | 104 + .../Portfolio/Overview/OpenLimitsTable.tsx | 249 + .../src/pages/Portfolio/Overview/Overview.tsx | 150 + .../Portfolio/Overview/OverviewTables.tsx | 53 + .../Portfolio/Overview/PortfolioChart.tsx | 218 + .../pages/Portfolio/Overview/StatsTiles.tsx | 66 + .../Portfolio/Overview/TableSectionHeader.tsx | 29 + .../Portfolio/Overview/ViewAllButton.tsx | 44 + .../src/pages/Portfolio/Overview/constants.ts | 6 + .../Overview/hooks/useIsPortfolioZero.ts | 24 + .../hooks/useSwapUSDValuesFromGraphQL.ts | 77 + .../Overview/hooks/useSwapsThisWeek.ts | 50 + .../utils/checkBalanceDiffWithinRange.ts | 30 + .../pages/Portfolio/PortfolioPageInner.tsx | 51 + .../Tokens/Table/TokensAllocationChart.tsx | 8 +- .../Tokens/Table/columns/Allocation.tsx | 11 +- .../Table/columns/ContextMenuButton.tsx | 35 +- .../Portfolio/Tokens/Table/columns/Price.tsx | 21 + .../Tokens/Table/columns/RelativeChange1D.tsx | 16 +- .../Tokens/Table/columns/TokenDisplay.tsx | 8 +- .../Portfolio/Tokens/Table/columns/Value.tsx | 24 +- .../Tokens/Table/columns/useTokenColumns.tsx | 188 + .../Tokens/hooks/useNavigateToTokenDetails.ts | 22 + .../AnimatedStyledBanner/AnimatedEmblems.tsx | 126 + .../AnimatedStyledBanner.tsx | 33 + .../AnimatedStyledBanner/Emblems/EmblemA.tsx | 29 + .../AnimatedStyledBanner/Emblems/EmblemB.tsx | 20 + .../AnimatedStyledBanner/Emblems/EmblemC.tsx | 103 + .../AnimatedStyledBanner/Emblems/EmblemD.tsx | 29 + .../AnimatedStyledBanner/Emblems/EmblemE.tsx | 20 + .../AnimatedStyledBanner/Emblems/EmblemF.tsx | 24 + .../AnimatedStyledBanner/Emblems/EmblemG.tsx | 20 + .../AnimatedStyledBanner/Emblems/EmblemH.tsx | 20 + .../AnimatedStyledBanner/Emblems/index.ts | 8 + .../AnimatedStyledBanner/Emblems/types.ts | 6 + .../components/PortfolioExpandoRow.tsx | 26 + .../Portfolio/components/SearchInput.tsx | 47 +- .../ValueWithFadedDecimals.tsx | 23 +- apps/web/src/pages/Portfolio/constants.ts | 7 + .../Portfolio/hooks/usePortfolioAddresses.ts | 24 + .../pages/Portfolio/utils/portfolioUrls.ts | 31 + .../Positions/ClaimFees.anvil.e2e.test.ts | 130 +- apps/web/src/pages/Positions/PositionPage.tsx | 32 + .../web/src/pages/Redirects.anvil.e2e.test.ts | 30 +- apps/web/src/pages/Redirects.e2e.test.ts | 114 +- .../RemoveLiquidity.anvil.e2e.test.ts | 43 +- .../RemoveLiquidity/RemoveLiquidityReview.tsx | 2 +- .../RemoveLiquidityTxContext.tsx | 2 +- apps/web/src/pages/RouteDefinitions.tsx | 25 +- apps/web/src/pages/Swap/Buy/BuyForm.tsx | 14 +- .../src/pages/Swap/Buy/CountryListModal.tsx | 2 +- .../Swap/Buy/OffRampConfirmTransferModal.tsx | 11 +- .../pages/Swap/Buy/ProviderConnectedView.tsx | 12 +- .../CountryListModal.test.tsx.snap | 196 +- .../ProviderConnectedView.test.tsx.snap | 11 +- .../ProviderConnectionError.test.tsx.snap | 6 +- apps/web/src/pages/Swap/Buy/shared.test.tsx | 7 +- apps/web/src/pages/Swap/Buy/shared.tsx | 11 +- .../pages/Swap/Limit/LimitExpirySection.tsx | 6 +- apps/web/src/pages/Swap/Limit/LimitForm.tsx | 22 +- .../src/pages/Swap/Limit/LimitPriceError.tsx | 16 +- .../LimitPriceError.test.tsx.snap | 250 +- .../pages/Swap/Send/NewAddressSpeedBump.tsx | 46 +- .../Swap/Send/SendCurrencyInputForm.test.tsx | 8 +- .../pages/Swap/Send/SendCurrencyInputForm.tsx | 6 +- apps/web/src/pages/Swap/Send/SendForm.tsx | 2 +- .../src/pages/Swap/Send/SendRecipientForm.tsx | 22 +- .../src/pages/Swap/Send/SendReviewModal.tsx | 2 +- .../Swap/Send/SmartContractSpeedBump.tsx | 30 +- .../NewAddressSpeedBump.test.tsx.snap | 283 +- .../SendCurrencyInputForm.test.tsx.snap | 1013 +- .../SendRecipientForm.test.tsx.snap | 160 +- .../SendReviewModal.test.tsx.snap | 26 +- .../SmartContractSpeedbump.test.tsx.snap | 267 +- apps/web/src/pages/Swap/Swap.e2e.test.ts | 46 +- .../src/pages/Swap/SwapSettings.e2e.test.ts | 46 +- .../src/pages/Swap/TokenSelector.e2e.test.ts | 112 +- .../src/pages/Swap/UniswapX.anvil.e2e.test.ts | 120 +- .../web/src/pages/Swap/Wrap.anvil.e2e.test.ts | 100 +- apps/web/src/pages/Swap/common/shared.tsx | 10 +- apps/web/src/pages/Swap/index.tsx | 10 +- .../TokenDetails/TokenDetails.e2e.test.ts | 113 +- .../src/pages/Wrapped/DisconnectedState.tsx | 144 + apps/web/src/pages/Wrapped/index.tsx | 130 + .../pages/__snapshots__/routes.test.ts.snap | 10 +- apps/web/src/pages/getPortfolioTitle.ts | 27 + apps/web/src/pages/paths.ts | 3 +- apps/web/src/playwright/fixtures/account.ts | 13 + apps/web/src/playwright/fixtures/urls.test.ts | 232 + apps/web/src/playwright/fixtures/urls.ts | 61 + .../mocks/dataApiService/get_portfolio.json | 99 + .../mocks/fiatOnRamp/get-country.json | 5 + .../playwright/mocks/fiatOnRamp/quotes.json | 69 + .../mocks/fiatOnRamp/supported-countries.json | 952 ++ .../fiatOnRamp/supported-fiat-currencies.json | 634 + .../mocks/fiatOnRamp/supported-tokens.json | 182 + .../mocks/graphql/Token/uni_token.json | 83 +- .../mocks/graphql/Token/uni_token_price.json | 3621 +++++ .../mocks/graphql/TokenWeb/uni_token.json | 99 +- apps/web/src/playwright/mocks/mocks.ts | 17 +- .../src/playwright/mocks/tradingApi/swap.json | 41 + apps/web/src/setupTests.ts | 33 +- apps/web/src/state/activity/updater.tsx | 4 + .../src/state/explore/protocolStats.test.tsx | 164 + apps/web/src/state/explore/protocolStats.ts | 176 +- apps/web/src/state/explore/topAuctions.ts | 295 + apps/web/src/state/migrations.ts | 8 +- apps/web/src/state/migrations/16.ts | 6 +- apps/web/src/state/migrations/19.test.ts | 4 +- apps/web/src/state/migrations/21.ts | 4 +- apps/web/src/state/migrations/22.test.ts | 2 + apps/web/src/state/migrations/25.ts | 2 +- apps/web/src/state/migrations/57.ts | 2 +- apps/web/src/state/migrations/58.test.ts | 176 + apps/web/src/state/migrations/58.ts | 73 + apps/web/src/state/migrations/59.test.ts | 21 + apps/web/src/state/migrations/59.ts | 20 + apps/web/src/state/migrations/60.test.ts | 26 + apps/web/src/state/migrations/60.ts | 19 + apps/web/src/state/migrations/oldTypes.ts | 4 +- apps/web/src/state/mint/v3/hooks.tsx | 1 + apps/web/src/state/outage/atoms.ts | 9 + apps/web/src/state/outage/types.ts | 7 + apps/web/src/state/reducerTypeTest.ts | 11 +- apps/web/src/state/routing/types.ts | 1 + apps/web/src/state/routing/utils.ts | 1 + .../sagas/lp_incentives/lpIncentivesSaga.ts | 15 +- .../src/state/sagas/lp_incentives/types.ts | 6 +- apps/web/src/state/sagas/transactions/5792.ts | 15 +- .../transactions/useSwapHandlers.test.ts | 470 + .../sagas/transactions/useSwapHandlers.ts | 115 + .../src/state/sagas/transactions/wrapSaga.ts | 2 +- apps/web/src/state/swap/hooks.tsx | 45 +- apps/web/src/state/swap/types.ts | 54 +- apps/web/src/state/swap/useSwapContext.tsx | 6 +- .../web/src/state/transactions/hooks.test.tsx | 114 + apps/web/src/state/transactions/hooks.tsx | 18 +- apps/web/src/state/user/types.ts | 2 +- .../hooks/useMismatchAccount.ts | 2 +- .../src/state/walletCapabilities/reducer.ts | 2 +- apps/web/src/test-utils/mockTamagui.ts | 205 + apps/web/src/theme/components/Dividers.tsx | 4 +- .../web/src/theme/components/FadePresence.tsx | 4 +- apps/web/src/theme/components/Links.tsx | 6 +- apps/web/src/theme/components/text.tsx | 6 +- apps/web/src/theme/zIndex.ts | 1 + apps/web/src/tracing/amplitude.ts | 7 + apps/web/src/tracing/swapFlowLoggers.test.ts | 2 + apps/web/src/tracing/swapFlowLoggers.ts | 12 + apps/web/src/utils/errors/isOutageError.ts | 81 + .../src/utils/filterDefinedWalletAddresses.ts | 22 + apps/web/src/utils/isIFramed.ts | 12 +- .../utils/showSwitchNetworkNotification.ts | 9 +- apps/web/tsconfig.json | 13 +- apps/web/vite.config.mts | 70 +- apps/web/vite/entry-gateway-proxy.ts | 150 + apps/web/vite/mockAssets.tsx | 2 +- apps/web/vite/vite.plugins.ts | 7 +- apps/web/vitest.config.ts | 1 + bunfig.toml | 1 + config/jest-presets/jest/setup.js | 11 + config/vitest-presets/package.json | 2 +- dangerfile.ts | 2 +- eslint-local-rules.js | 1 + index.d.ts | 10 + packages/api/project.json | 14 +- packages/api/scripts/fixGraphQLApiTypes.mts | 91 + .../api/scripts/modifyTradingApiTypes.mts | 182 +- .../api/src/clients/base/createFetchClient.ts | 28 +- packages/api/src/clients/base/types.ts | 7 +- packages/api/src/clients/base/urls.ts | 5 + .../blockaid/createBlockaidApiClient.ts | 185 + packages/api/src/clients/blockaid/types.ts | 495 + .../data/createDataServiceApiClient.ts | 14 +- .../api/src/clients/graphql/codegen.config.ts | 38 +- packages/api/src/clients/graphql/fragments.ts | 6 +- packages/api/src/clients/graphql/generated.ts | 5 + packages/api/src/clients/graphql/queries.ts | 1 - .../api/src/clients/graphql/web/pool.graphql | 1 + .../api/src/clients/graphql/web/token.graphql | 2 + .../liquidity/createLiquidityServiceClient.ts | 71 + packages/api/src/clients/trading/api.json | 2 +- .../clients/trading/createTradingApiClient.ts | 137 +- .../unitags/createUnitagsApiClient.test.ts | 1 + packages/api/src/components/ApiInit.test.tsx | 359 + packages/api/src/components/ApiInit.tsx | 53 + packages/api/src/connectRpc/utils.ts | 2 +- packages/api/src/getEntryGatewayUrl.ts | 40 + .../api/src/getIsSessionServiceEnabled.ts | 7 - packages/api/src/getSessionService.native.ts | 58 - packages/api/src/getSessionService.ts | 6 - packages/api/src/getSessionService.web.ts | 68 - packages/api/src/provideDeviceIdService.ts | 25 + .../api/src/provideSessionService.native.ts | 46 + packages/api/src/provideSessionService.ts | 11 + packages/api/src/provideSessionService.web.ts | 76 + packages/api/src/provideSessionStorage.ts | 21 + .../src/provideUniswapIdentifierService.ts | 25 + .../storage/createExtensionStorageDriver.ts | 29 + .../src/storage/createNativeStorageDriver.ts | 19 + .../api/src/storage/createWebStorageDriver.ts | 17 + .../src/storage/getStorageDriver.native.ts | 6 + packages/api/src/storage/getStorageDriver.ts | 8 + .../api/src/storage/getStorageDriver.web.ts | 11 + packages/api/src/storage/types.ts | 9 + packages/api/src/transport.ts | 85 + packages/api/tsconfig.json | 10 +- packages/api/vitest.config.ts | 6 +- packages/biome-config/base.jsonc | 20 +- packages/biome-config/package.json | 6 +- packages/biome-config/project.json | 26 + packages/biome-config/src/extractor.test.js | 138 + .../src/fixtures/array-merge-config.jsonc | 52 + .../src/fixtures/no-markers-config.jsonc | 37 + .../src/fixtures/off-override-config.jsonc | 41 + .../src/fixtures/simple-config.jsonc | 45 + packages/biome-config/src/merger.js | 44 +- packages/biome-config/src/merger.test.js | 178 + packages/biome-config/src/processor.js | 47 +- packages/biome-config/src/processor.test.js | 336 + packages/biome-config/src/universePackages.js | 146 + packages/config/package.json | 4 +- packages/config/src/config-types.ts | 8 + packages/config/src/getConfig.native.ts | 32 +- packages/config/src/getConfig.web.ts | 23 +- packages/config/src/global.d.ts | 7 + packages/eslint-config/base.js | 8 + packages/eslint-config/native.js | 3 +- packages/eslint-config/package.json | 6 +- .../no-transform-percentage-strings.js | 192 + .../no-transform-percentage-strings.test.js | 228 + packages/eslint-config/restrictedImports.js | 6 +- packages/gating/src/configs.ts | 17 - packages/gating/src/flags.ts | 88 +- .../gating/src/getIsSessionServiceEnabled.ts | 14 + .../src/getIsSessionUpgradeAutoEnabled.ts | 9 + .../src/getIsNotificationServiceEnabled.ts | 7 + packages/notifications/src/global.d.ts | 2 + packages/notifications/src/index.ts | 29 + .../NotificationDataSource.ts | 8 + .../getNotificationQueryOptions.ts | 74 + .../createNotificationDataSource.ts | 19 + ...reatePollingNotificationDataSource.test.ts | 356 + .../createPollingNotificationDataSource.ts | 74 + .../NotificationProcessor.ts | 26 + .../createBaseNotificationProcessor.test.ts | 854 ++ .../createBaseNotificationProcessor.ts | 297 + .../createNotificationProcessor.test.ts | 130 + .../createNotificationProcessor.ts | 15 + .../NotificationRenderer.ts | 8 + .../components/BannerTemplate.tsx | 134 + .../components/InlineBannerNotification.tsx | 62 + .../createNotificationRenderer.ts | 16 + .../NotificationService.ts | 47 + .../createNotificationService.test.ts | 1092 ++ .../createNotificationService.ts | 364 + .../NotificationTelemetry.ts | 44 + .../createNotificationTelemetry.test.ts | 99 + .../createNotificationTelemetry.ts | 33 + .../NotificationTracker.ts | 14 + .../createApiNotificationTracker.test.ts | 399 + .../createApiNotificationTracker.ts | 146 + .../createNoopNotificationTracker.ts | 44 + .../createNotificationTracker.ts | 31 + .../src/utils/formatNotificationType.test.ts | 25 + .../src/utils/formatNotificationType.ts | 23 + packages/notifications/vitest-setup.ts | 7 + packages/notifications/vitest.config.ts | 14 + packages/sessions/package.json | 19 +- .../createChallengeSolverService.ts | 47 + .../createHashcashMockSolver.ts | 39 + .../createHashcashSolver.test.ts | 145 + .../challenge-solvers/createHashcashSolver.ts | 91 + .../challenge-solvers/createNoneMockSolver.ts | 11 + .../createTurnstileMockSolver.ts | 30 + .../createTurnstileSolver.ts | 177 + .../challenge-solvers/hashcash/core.test.ts | 262 + .../src/challenge-solvers/hashcash/core.ts | 124 + .../turnstileSolver.integration.test.ts | 329 + .../sessions/src/challenge-solvers/types.ts | 17 + .../src/challengeFlow.integration.test.ts | 393 + .../src/device-id/createDeviceIdService.ts | 17 +- packages/sessions/src/device-id/types.ts | 10 +- packages/sessions/src/index.ts | 41 +- ...createSessionInitializationService.test.ts | 354 + .../createSessionInitializationService.ts | 195 + .../session-initialization/sessionErrors.ts | 32 + .../session-repository/createSessionClient.ts | 2 +- .../createSessionRepository.test.ts | 40 +- .../createSessionRepository.ts | 95 +- .../src/session-repository/transport.ts | 38 - .../sessions/src/session-repository/types.ts | 13 +- .../createNoopSessionService.ts | 11 +- .../createSessionService.test.ts | 111 + .../session-service/createSessionService.ts | 35 +- .../sessions/src/session-service/types.ts | 42 +- .../session-storage/createSessionStorage.ts | 3 + .../sessions/src/session-storage/types.ts | 4 + .../sessions/src/session.integration.test.ts | 246 + .../src/sessionLifecycle.integration.test.ts | 263 + packages/sessions/src/test-utils.ts | 210 + .../test-utils/createLocalCookieTransport.ts | 52 + packages/sessions/src/test-utils/mocks.ts | 120 + .../createUniswapIdentifierService.ts | 19 + .../sessions/src/uniswap-identifier/types.ts | 11 + packages/sessions/vitest.config.ts | 2 + packages/ui/package.json | 48 +- .../components/AnimatePresencePager.tsx | 6 +- .../animations/components/HeightAnimator.tsx | 21 +- .../animations/components/WidthAnimator.tsx | 19 +- .../hooks/useShakeAnimation.test.tsx | 6 +- .../assets/backgrounds/dots-banner-dark.png | Bin 0 -> 6978 bytes .../assets/backgrounds/dots-banner-light.png | Bin 0 -> 7118 bytes .../backgrounds/monad-test-banner-light.png | Bin 0 -> 29178 bytes .../bridged-assets-v2-card-banner-dark.png | Bin 0 -> 81953 bytes .../bridged-assets-v2-card-banner-light.png | Bin 0 -> 76561 bytes .../ui/src/assets/graphics/zero-percent.png | Bin 0 -> 6446 bytes packages/ui/src/assets/icons/approve-alt.svg | 3 + .../src/assets/icons/avatar-placeholder.svg | 3 + .../ui/src/assets/icons/block-explorer.svg | 2 +- packages/ui/src/assets/icons/box.svg | 3 + .../ui/src/assets/icons/chart-bar-crossed.svg | 4 + packages/ui/src/assets/icons/credit-card.svg | 6 + .../ui/src/assets/icons/crosschain-icon.svg | 4 + packages/ui/src/assets/icons/eth-mini.svg | 4 + packages/ui/src/assets/icons/gift.svg | 3 + packages/ui/src/assets/icons/money-hand.svg | 3 + packages/ui/src/assets/icons/receipt.svg | 3 + packages/ui/src/assets/icons/send-alt.svg | 3 + .../assets/icons/shield-magnifying-glass.svg | 3 + packages/ui/src/assets/icons/snowflake.svg | 3 + .../assets/logos/png/monad-logo-filled.png | Bin 0 -> 2017 bytes .../ui/src/assets/logos/png/monad-logo.png | Bin 1881 -> 0 bytes .../assets/logos/svg/solscan-logo-dark.svg | 6 +- .../assets/logos/svg/solscan-logo-light.svg | 6 +- .../OverKeyboardContent.native.tsx | 9 +- .../RefreshButton/RefreshButton.native.tsx | 26 + .../RefreshButton/RefreshButton.tsx | 11 + .../RefreshButton/RefreshButton.web.tsx | 79 + .../RefreshButton/RefreshButtonIcon.tsx | 73 + .../SegmentedControl/SegmentedControl.tsx | 14 +- .../src/components/buttons/Button/Button.tsx | 3 +- .../DropdownMenuSheetItem.tsx | 16 +- .../src/components/factories/createIcon.tsx | 17 +- .../ui/src/components/icons/ApproveAlt.tsx | 17 + .../components/icons/AvatarPlaceholder.tsx | 14 + .../ui/src/components/icons/BlockExplorer.tsx | 1 - packages/ui/src/components/icons/Box.tsx | 17 + .../src/components/icons/ChartBarCrossed.tsx | 17 + .../ui/src/components/icons/CreditCard.tsx | 16 + .../src/components/icons/CrosschainIcon.tsx | 18 + packages/ui/src/components/icons/EthMini.tsx | 21 + packages/ui/src/components/icons/Gift.tsx | 16 + .../ui/src/components/icons/MoneyHand.tsx | 17 + packages/ui/src/components/icons/Receipt.tsx | 17 + packages/ui/src/components/icons/SendAlt.tsx | 17 + .../icons/ShieldMagnifyingGlass.tsx | 16 + .../ui/src/components/icons/Snowflake.tsx | 18 + packages/ui/src/components/icons/exported.ts | 13 + .../lines/VerticalDottedLineSeparator.tsx | 33 + .../src/components/logos/SolscanLogoDark.tsx | 5 +- .../src/components/logos/SolscanLogoLight.tsx | 3 +- .../src/components/modal/AdaptiveWebModal.tsx | 23 +- .../popover/AdaptiveWebPopoverContent.tsx | 5 +- .../ClickableWithinGesture.web.tsx | 12 +- .../ui/src/components/switch/Switch.web.tsx | 6 +- packages/ui/src/components/text/Text.tsx | 14 +- .../ui/src/components/tooltip/Tooltip.web.tsx | 43 +- .../touchable/TouchableArea/TouchableArea.tsx | 37 +- .../TouchableArea/TouchableAreaFrame.tsx | 4 - .../TouchableTextLink/TouchableTextLink.tsx | 2 +- .../src/hooks/usePreventOverflowBelowFold.tsx | 2 +- packages/ui/src/index.ts | 2 + packages/ui/src/theme/color/colors.ts | 4 +- packages/ui/src/theme/fonts.ts | 24 + packages/ui/src/theme/zIndexes.ts | 2 +- packages/ui/src/utils/colors/index.ts | 2 +- .../ui/src/utils/colors/specialCaseTokens.ts | 4 + packages/ui/vitest.config.ts | 10 +- packages/uniswap/.gitignore | 1 + packages/uniswap/babel.config.js | 9 +- packages/uniswap/jest-setup.js | 7 + packages/uniswap/jest.config.js | 6 - packages/uniswap/project.json | 9 +- .../AnimatedNumber/AnimatedNumber.tsx | 2 +- .../src/components/BaseCard/BaseCard.tsx | 10 +- .../__snapshots__/BaseCard.test.tsx.snap | 147 +- .../BridgedAsset/BridgedAssetModal.tsx | 2 +- .../ConfirmSwapModal/ProgressIndicator.tsx | 156 +- .../ConfirmSwapModal/steps/Approve.tsx | 33 +- .../components/ConfirmSwapModal/steps/LP.tsx | 12 +- .../ConfirmSwapModal/steps/Permit.tsx | 31 +- .../steps/StepRowSkeleton.tsx | 177 +- .../ConfirmSwapModal/steps/Swap.tsx | 74 +- .../steps/SwapTXPlanStepRow.tsx | 171 + .../ConfirmSwapModal/steps/Wrap.tsx | 9 +- .../useSecondsUntilDeadline.tsx | 48 + .../CurrencyInputPanel/CurrencyInputPanel.tsx | 6 + .../CurrencyInputPanelBalance.tsx | 6 +- .../CurrencyInputPanelInput.tsx | 50 +- .../TokenOptionItem/TokenOptionItem.web.tsx | 3 +- .../CurrencyInputPanel/TokenRate.tsx | 2 +- .../hooks/useInputFocusSync/types.ts | 2 +- .../components/CurrencyInputPanel/types.tsx | 4 +- .../components/CurrencyLogo/NetworkLogo.tsx | 19 +- .../LogoWithTxStatus.test.tsx.snap | 27 +- .../__snapshots__/ExpandoRow.test.tsx.snap | 168 +- .../ReceiveQRCode/ReceiveQRCode.tsx | 9 +- .../RelativeChange/RelativeChange.tsx | 4 +- .../RoutingDiagram/RoutingDiagram.tsx | 10 +- .../TokenSelector/TokenSelector.tsx | 24 +- .../TokenSelector/TokenSelectorList.tsx | 14 +- .../UnsupportedChainedActionsBanner.tsx | 67 + .../components/TokenSelector/hooks.test.ts | 1 - .../hooks/useAllCommonBaseCurrencies.ts | 1 + .../TokenSelector/items/tokens/TokenCard.tsx | 7 +- .../WarningMessage/WarningMessage.tsx | 3 +- .../components/accounts/AddressDisplay.tsx | 8 +- .../activity/ActivityListEmptyState.tsx | 37 + .../details/TransactionDetailsModal.test.tsx | 25 +- .../details/TransactionDetailsModal.tsx | 28 +- .../TransactionDetailsModal.test.tsx.snap | 574 +- .../ApproveTransactionDetails.tsx | 15 +- .../NftTransactionDetails.test.tsx.snap | 128 +- .../SwapTransactionDetails.test.tsx.snap | 152 +- .../TransferTransactionDetails.test.tsx.snap | 74 +- .../activity/generateActivityItemRenderer.ts | 3 + .../useFormattedCurrencyAmountAndUSDValue.ts | 7 +- .../activity/summaries/ApproveSummaryItem.tsx | 28 +- .../activity/summaries/BridgeSummaryItem.tsx | 10 +- .../summaries/LiquiditySummaryItem.tsx | 11 +- .../summaries/NFTApproveSummaryItem.tsx | 10 +- .../activity/summaries/NFTMintSummaryItem.tsx | 10 +- .../activity/summaries/NFTSummaryItem.tsx | 2 + .../summaries/NFTTradeSummaryItem.tsx | 10 +- .../summaries/OffRampTransferSummaryItem.tsx | 2 + .../summaries/OnRampTransferSummaryItem.tsx | 10 +- .../activity/summaries/ReceiveSummaryItem.tsx | 2 + .../activity/summaries/SendSummaryItem.tsx | 2 + .../activity/summaries/SwapSummaryItem.tsx | 10 +- .../summaries/TransactionSummaryLayout.tsx | 31 +- .../summaries/TransferTokenSummaryItem.tsx | 11 +- .../activity/summaries/UnknownSummaryItem.tsx | 11 +- .../activity/summaries/WCSummaryItem.tsx | 2 + .../activity/summaries/WrapSummaryItem.tsx | 11 +- .../uniswap/src/components/activity/types.ts | 4 +- .../uniswap/src/components/activity/utils.ts | 37 + .../UniswapWrapped2025Banner.native.tsx | 85 + .../UniswapWrapped2025Banner.tsx | 6 + .../UniswapWrapped2025Banner.web.tsx | 174 + .../banners/UniswapWrapped2025Banner/types.ts | 5 + .../UniswapWrapped2025Card.native.tsx | 69 + .../UniswapWrapped2025Card.tsx | 6 + .../UniswapWrapped2025Card.web.tsx | 102 + .../banners/UniswapWrapped2025Card/types.ts | 3 + .../shared/SharedSnowflakeComponents.tsx | 199 + .../src/components/banners/shared/utils.ts | 5 + .../__snapshots__/PasteButton.test.tsx.snap | 172 +- .../components/chains}/BlockExplorerIcon.tsx | 2 +- .../src/components/dialog/Dialog.native.tsx | 122 +- .../src/components/dialog/Dialog.test.tsx | 149 +- .../src/components/dialog/Dialog.web.tsx | 177 +- .../src/components/dialog/DialogButtons.tsx | 60 + .../src/components/dialog/DialogContent.tsx | 81 + .../src/components/dialog/DialogProps.tsx | 36 +- .../src/components/dialog/GetHelpButtonUI.tsx | 33 + .../dialog/GetHelpHeader.native.tsx | 22 + .../src/components/dialog/GetHelpHeader.tsx | 76 +- .../components/dialog/GetHelpHeader.web.tsx | 26 + .../dialog/GetHelpHeaderContent.tsx | 54 + .../dialog/hooks/useDialogVisibility.test.ts | 299 + .../dialog/hooks/useDialogVisibility.ts | 131 + .../dropdowns/ActionSheetDropdown.tsx | 164 +- .../ActionSheetDropdown.test.tsx.snap | 72 +- .../__snapshots__/NetworkFee.test.tsx.snap | 231 +- .../src/components/lists/SelectorBaseList.tsx | 2 +- ...NftSearchResultsToNftCollectionOptions.tsx | 18 +- .../lists/items/tokens/TokenOptionItem.tsx | 4 +- .../tokens/TokenOptionItemContextMenu.tsx | 2 +- .../src/components/lists/items/types.ts | 2 +- .../components/logos/PoweredByBlockaid.tsx | 33 + .../components/menus/ContextMenuContent.tsx | 82 +- .../components/menus/ContextMenuV2.native.tsx | 73 +- .../src/components/menus/ContextMenuV2.tsx | 7 + .../components/menus/ContextMenuV2.web.tsx | 33 +- .../ContextMenuV2.web.test.tsx.snap | 356 +- .../menus/hooks/useContextMenuTracking.ts | 51 + .../src/components/modals/InfoLinkModal.tsx | 8 +- .../src/components/modals/Modal.native.tsx | 6 +- .../src/components/modals/Modal.web.tsx | 3 +- .../src/components/modals/ModalProps.tsx | 1 + .../modals/WarningModal/WarningModal.tsx | 100 +- .../components/modals/WarningModal/types.ts | 1 + .../modals/useBottomSheetSafeKeyboard.web.tsx | 4 +- .../__snapshots__/NetworkFilter.test.tsx.snap | 123 +- .../src/components/nfts/NftsList.native.tsx | 15 +- .../components/nfts/NftsListEmptyState.tsx | 37 + .../src/components/nfts/NftsListHeader.tsx | 105 + .../src/components/nfts/ShowNFTModal.tsx | 4 +- .../nfts/hooks/useNftListRenderData.ts | 33 +- .../src/components/nfts/images/NFTViewer.tsx | 10 +- packages/uniswap/src/components/nfts/types.ts | 15 + .../notifications/ModalTemplate.tsx | 327 + .../notifications/MonadAnnouncementModal.tsx | 57 + .../notifications/CopiedNotification.tsx | 19 +- .../portfolio/PortfolioEmptyState.tsx | 6 +- .../TokenBalanceItemContextMenu.native.tsx | 5 + .../portfolio/TokenBalanceItemContextMenu.tsx | 2 + .../TokenBalanceItemContextMenu.web.tsx | 14 +- .../portfolio/TokenBalanceListWeb.tsx | 79 +- .../src/components/reporting/ReportModal.tsx | 128 + .../reporting/ReportPoolDataModal.tsx | 105 + .../reporting/ReportTokenDataModal.tsx | 101 + .../reporting/ReportTokenIssueModal.tsx | 132 + .../src/components/reporting/input.tsx | 28 + .../src/components/text/LearnMoreLink.tsx | 3 + .../__snapshots__/LearnMoreLink.test.tsx.snap | 24 +- .../tokens/TokensListEmptyState.tsx | 37 + .../components/tooltip/InfoTooltip.web.tsx | 8 +- packages/uniswap/src/constants/routing.ts | 12 +- packages/uniswap/src/constants/tokens.ts | 8 +- packages/uniswap/src/constants/urls.ts | 8 +- .../blockaidApi/BlockaidApiClient.ts | 14 + .../apiClients/createUniswapFetchClient.ts | 12 +- .../LiquidityServiceClient.ts | 15 + .../useMigrateV2ToV3LPPositionQuery.ts | 35 + .../useMigrateV3ToV4LPPositionQuery.ts | 35 + .../tradingApi/TradingApiClient.test.ts | 106 +- .../useMigrateV3LpPositionCalldataQuery.ts | 2 +- .../tradingApi/useWalletEncode7702Query.ts | 17 +- .../apiClients/unitagsApi/UnitagsApiClient.ts | 12 +- packages/uniswap/src/data/cache.ts | 19 +- packages/uniswap/src/data/links.ts | 2 +- .../src/data/rest/auctions/auctionService.ts | 44 + .../uniswap/src/data/rest/auctions/paths.ts | 12 + .../uniswap/src/data/rest/auctions/types.ts | 80 + .../auctions/useGetAuctionDetailsQuery.ts | 31 + .../data/rest/auctions/useGetAuctionsQuery.ts | 32 + .../auctions/useGetBidConcentrationQuery.ts | 31 + .../useGetBidsByWalletInfiniteQuery.ts | 35 + .../rest/auctions/useGetBidsByWalletQuery.ts | 32 + .../auctions/useGetLatestCheckpointQuery.ts | 31 + packages/uniswap/src/data/rest/base.ts | 30 +- packages/uniswap/src/data/rest/getPools.ts | 4 +- .../src/data/rest/getPortfolioChart.ts | 64 + .../uniswap/src/data/rest/getPositions.ts | 2 +- .../uniswap/src/data/rest/listTransactions.ts | 80 +- .../DialogPreferencesService.ts | 47 + .../createDialogPreferencesService.test.ts | 173 + .../createDialogPreferencesService.ts | 64 + .../uniswap/src/dialog-preferences/index.ts | 8 + .../uniswap/src/dialog-preferences/types.ts | 5 + .../src/features/accounts/AccountIcon.tsx | 5 +- .../activity/extract/conversion.test.ts | 1 - .../activity/extract/conversionRest.test.ts | 10 +- .../activity/hooks/useActivityData.test.tsx | 558 + .../activity/hooks/useActivityData.tsx | 40 +- ...ormattedTransactionDataForActivity.test.ts | 302 + .../useFormattedTransactionDataForActivity.ts | 51 +- ...useMergeLocalAndRemoteTransactions.test.ts | 248 + .../activity/hooks/useTransactionActions.tsx | 166 +- .../parse/parseBridgingTransaction.ts | 6 +- .../parse/parseLiquidityTransaction.ts | 8 +- .../activity/parse/parseMintTransaction.ts | 10 +- .../activity/parse/parseReceiveTransaction.ts | 3 + .../activity/parse/parseSendTransaction.ts | 2 + .../activity/parse/parseTradeTransaction.ts | 29 +- .../activity/parse/parseUnknownTransaction.ts | 8 +- .../activity/utils/extractDappInfo.ts | 25 + .../utils/getTransactionSummaryTitle.ts | 3 +- .../src/features/behaviorHistory/selectors.ts | 3 + .../src/features/chains/chainInfo.test.ts | 6 +- .../uniswap/src/features/chains/chainInfo.ts | 4 +- .../src/features/chains/evm/info/arbitrum.ts | 2 + .../src/features/chains/evm/info/avalanche.ts | 1 + .../src/features/chains/evm/info/base.ts | 2 + .../src/features/chains/evm/info/blast.ts | 2 + .../src/features/chains/evm/info/bnb.ts | 2 + .../src/features/chains/evm/info/celo.ts | 1 + .../src/features/chains/evm/info/mainnet.ts | 3 + .../src/features/chains/evm/info/optimism.ts | 2 + .../src/features/chains/evm/info/polygon.ts | 2 + .../src/features/chains/evm/info/soneium.ts | 2 + .../src/features/chains/evm/info/unichain.ts | 3 + .../features/chains/evm/info/worldchain.ts | 2 + .../src/features/chains/evm/info/zksync.ts | 2 + .../src/features/chains/evm/info/zora.ts | 2 + .../uniswap/src/features/chains/evm/rpc.ts | 4 +- .../chains/hooks/useFeatureFlaggedChainIds.ts | 12 +- .../features/chains/hooks/useNewChainIds.ts | 5 +- .../uniswap/src/features/chains/logos.tsx | 2 +- .../src/features/chains/svm/info/solana.ts | 1 + packages/uniswap/src/features/chains/types.ts | 9 +- .../uniswap/src/features/chains/utils.test.ts | 9 +- packages/uniswap/src/features/chains/utils.ts | 12 +- .../features/dataApi/balances/balancesRest.ts | 8 +- .../listTransactions/listTransactions.test.ts | 28 +- .../listTransactions/listTransactions.ts | 121 +- .../tokenDetails/useTokenDetailsData.ts | 94 + .../dataApi/utils/usePersistedError.ts | 2 +- .../PaymentMethodFilter.test.tsx.snap | 148 +- .../fiatOnRamp/UnsupportedTokenModal.tsx | 60 +- .../src/features/language/constants.ts | 198 +- .../uniswap/src/features/language/hooks.tsx | 120 - .../src/features/language/localizedDayjs.ts | 25 - .../nfts/hooks/useGroupNftsByVisibility.ts | 24 +- .../uniswap/src/features/nfts/utils.test.ts | 257 + packages/uniswap/src/features/nfts/utils.ts | 66 + .../src/features/notifications/slice/types.ts | 11 +- .../PortfolioBalance/RefreshBalanceButton.tsx | 120 - .../portfolio/TokenBalanceListContext.tsx | 17 +- .../hooks/useTokenContextMenuOptions.ts | 52 +- .../portfolio/portfolioUpdates/constants.ts | 6 +- .../isInstantTokenBalanceUpdateEnabled.ts | 5 - .../uniswap/src/features/reporting/reports.ts | 196 + .../SearchModal/SearchModalNoQueryList.tsx | 198 +- .../SearchModal/SearchModalResultsList.tsx | 15 +- .../hooks/useRecentlySearchedOptions.tsx | 23 +- .../hooks/useSectionsForNoQuerySearch.tsx | 165 + .../SearchModal/hooks/useWebSearchTabs.ts | 7 + .../src/features/search/SearchModal/types.ts | 1 + .../src/features/settings/constants.ts | 18 +- .../uniswap/src/features/settings/hooks.ts | 7 +- .../src/features/settings/selectors.ts | 7 + .../uniswap/src/features/settings/slice.ts | 6 + .../features/telemetry/constants/interface.ts | 4 + .../telemetry/constants/trace/modal.ts | 7 + .../telemetry/constants/trace/page.ts | 1 + .../features/telemetry/constants/wallet.ts | 4 +- .../uniswap/src/features/telemetry/types.ts | 127 +- .../uniswap/src/features/telemetry/user.ts | 1 + .../{ => warnings}/TokenWarningCard.tsx | 6 +- .../{ => warnings}/TokenWarningFlagsTable.tsx | 5 +- .../{ => warnings}/TokenWarningModal.tsx | 127 +- .../WarningInfoModalContainer.tsx | 3 - .../useBlockaidFeeComparisonAnalytics.ts | 6 +- .../useWarningModalCurrenciesDismissed.ts | 60 + .../tokens/{ => warnings}/safetyUtils.test.ts | 5 +- .../tokens/{ => warnings}/safetyUtils.ts | 16 +- .../tokens/{ => warnings}/slice/hooks.ts | 33 +- .../tokens/{ => warnings}/slice/selectors.ts | 12 +- .../tokens/{ => warnings}/slice/slice.ts | 37 +- .../tokens/{ => warnings}/slice/types.ts | 12 +- .../src/features/tokens/warnings/types.ts | 14 + .../TransactionDetails/SwapFee.tsx | 97 +- .../SwapReviewTokenWarningCard.tsx | 4 +- .../TransactionDetails/TransactionDetails.tsx | 26 +- .../modals/FeeOnTransferWarning.tsx | 6 +- .../modals/SwapFeeWarning.tsx | 22 +- .../transactions/TransactionDetails/types.ts | 2 +- .../utils/getFeeSeverity.ts | 7 +- .../utils/getRelevantTokenWarningSeverity.ts | 2 +- .../utils/getShouldDisplayTokenWarningCard.ts | 3 +- .../DecimalPadInput/DecimalPad.native.tsx | 11 +- .../DecimalPadInput/DecimalPadInput.tsx | 43 +- .../BuyNativeTokenButton.tsx | 21 +- .../InsufficientNativeTokenBaseComponent.tsx | 37 +- .../InsufficientNativeTokenWarning.tsx | 1 - .../TransactionModalContext.tsx | 3 +- .../TransactionModalProps.tsx | 3 +- .../settings/TransactionSettingsButton.tsx | 14 +- .../src/features/transactions/errors.ts | 1 + .../hooks/usePollingIntervalByChain.ts | 10 - .../steps/generateLPTransactionSteps.test.ts | 129 +- .../steps/generateLPTransactionSteps.ts | 71 +- .../liquidity/steps/increaseLiquiditySteps.ts | 18 +- .../liquidity/steps/increasePosition.ts | 22 +- .../transactions/liquidity/steps/migrate.ts | 2 +- .../liquidity/steps/migrationSteps.ts | 8 + .../features/transactions/liquidity/types.ts | 17 +- .../modals/CompatibleAddressModal.tsx | 2 +- .../modals/HiddenTokenInfoModal.tsx | 2 +- .../modals/MaxBalanceInfoModal.tsx | 16 +- .../src/features/transactions/selectors.ts | 2 +- .../src/features/transactions/slice.test.ts | 1 - .../src/features/transactions/slice.ts | 6 +- .../features/transactions/steps/approve.ts | 25 +- .../transactions/steps/permit2Signature.ts | 6 +- .../transactions/steps/permit2Transaction.ts | 3 +- .../src/features/transactions/steps/revoke.ts | 24 +- .../features/transactions/swap/analytics.ts | 8 +- .../swap/components/BridgingCurrencyRow.tsx | 2 +- .../swap/components/EstimatedBridgeTime.tsx | 42 +- .../swap/components/SwapArrowButton.tsx | 7 +- .../SwapFormButton/SwapFormButton.tsx | 5 +- .../SwapFormButton/hooks/useInterfaceWrap.ts | 80 - .../hooks/useIsSwapButtonDisabled.ts | 5 - .../hooks/useSwapFormButtonText.ts | 6 +- .../UnichainInstantBalanceModal/constants.ts | 1 + .../SwapFormCurrencyOutputPanel.tsx | 6 +- .../SwapFormDecimalPad.native.tsx | 3 +- .../form/SwapFormScreen/SwapFormScreen.tsx | 6 +- .../SwapFormScreenDetails/ExpandableRows.tsx | 7 +- .../SwapFormScreenDetails.web.tsx | 5 +- .../GasAndWarningRows/SwapWarningModal.tsx | 16 +- .../TradeInfoRow/GasInfoRow.test.tsx | 152 + .../TradeInfoRow/TradeWarning.tsx | 14 +- .../YouReceiveDetailsTooltip.tsx | 7 +- .../SwapFormWarningModals.tsx | 2 +- .../SwapFormScreenStoreContextProvider.tsx | 18 +- .../createSwapFormScreenStore.ts | 2 +- .../useSwapFormScreenCallbacks.ts | 42 +- .../swap/hooks/useNeedsBridgedAssetWarning.ts | 2 +- .../swap/hooks/usePriceDifference.test.ts | 10 +- .../getAztecUnavailableWarning.ts | 47 + ...usePrefilledNeedsTokenProtectionWarning.ts | 12 +- .../useSwapWarnings/useSwapWarnings.test.ts | 28 +- .../hooks/useSwapWarnings/useSwapWarnings.tsx | 16 +- .../transactions/swap/hooks/useTrade.ts | 3 + .../swap/hooks/useTrade/logging.ts | 2 +- .../features/transactions/swap/plan/types.ts | 73 + .../review/SwapDetails/AcceptNewQuoteRow.tsx | 36 +- .../swap/review/SwapDetails/SwapDetails.tsx | 26 +- .../DelayedSubmissionText.tsx | 33 + .../SwapReviewFooter/PendingSwapButton.tsx | 151 + .../SwapReviewFooter/SubmitSwapButton.tsx | 38 +- .../SwapReviewFooter/SwapReviewFooter.tsx | 14 +- .../SwapReviewScreen/SwapReviewScreen.tsx | 6 +- .../SwapReviewWrapTransactionDetails.tsx | 4 +- .../hooks/useCreateSwapReviewCallbacks.tsx | 98 +- .../chainedActionTxSwapAndGasInfoService.ts | 98 +- .../uniswapx/utils.test.tsx | 3 +- .../swapTxAndGasInfoService/utils.test.ts | 8 +- .../swap/services/executeSwapService.ts | 196 +- .../swap/services/hooks/useExecuteSwap.ts | 42 +- .../swap/services/hooks/usePrepareSwap.ts | 4 - .../swap/services/prepareSwapService.ts | 11 - .../services/tradeService/evmTradeService.ts | 4 + .../services/tradeService/svmTradeService.ts | 3 +- .../services/tradeService/tradeService.ts | 1 + .../transformations/buildQuoteRequest.ts | 2 - .../transformations/transformQuoteToTrade.ts | 6 +- .../transactions/swap/steps/signOrder.ts | 18 + .../features/transactions/swap/steps/swap.ts | 19 + .../SwapDependenciesStoreContextProvider.tsx | 15 +- .../createSwapDependenciesStore.ts | 6 - .../hooks/useOnToggleIsFiatMode.ts | 12 +- .../swap/types/getTradingApiSwapFee.ts | 4 +- .../transactions/swap/types/swapCallback.ts | 17 +- .../transactions/swap/types/swapHandlers.ts | 10 +- .../swap/types/swapTxAndGasInfo.ts | 16 +- .../features/transactions/swap/types/trade.ts | 6 +- .../transactions/swap/utils/chainedActions.ts | 34 + .../generateSwapTransactionSteps.test.ts | 22 +- .../utils/generateSwapTransactionSteps.ts | 17 +- .../transactions/swap/utils/getIdForQuote.ts | 10 + .../swap/utils/getPriceImpact.test.ts | 1 + .../features/transactions/swap/utils/trade.ts | 8 + .../swap/utils/tradingApi.test.ts | 3 +- .../transactions/swap/utils/tradingApi.ts | 21 +- .../transactions/types/transactionDetails.ts | 45 + .../features/unitags/ClaimUnitagContent.tsx | 13 +- .../__snapshots__/UnitagName.test.tsx.snap | 142 +- .../src/features/unitags/fileUtils.web.ts | 15 + .../visibility/hooks/useIsActivityHidden.ts | 7 + .../visibility/hooks/useTokenVisibility.ts | 20 + .../src/features/visibility/selectors.ts | 20 +- .../uniswap/src/features/visibility/slice.ts | 20 +- .../features/visibility/visibility.test.ts | 118 +- .../src/hooks/useIsKeyboardOpen.test.ts | 2 +- .../src/hooks/useShouldShowAztecWarning.ts | 18 + .../src/hooks/useSnowflakeAnimation.ts | 477 + packages/uniswap/src/i18n/i18n-setup.tsx | 8 - .../src/i18n/locales/translations/af-ZA.json | 2563 ---- .../src/i18n/locales/translations/ar-SA.json | 2563 ---- .../src/i18n/locales/translations/ca-ES.json | 2563 ---- .../src/i18n/locales/translations/cs-CZ.json | 2384 ---- .../src/i18n/locales/translations/da-DK.json | 2563 ---- .../src/i18n/locales/translations/de-DE.json | 2384 ---- .../src/i18n/locales/translations/el-GR.json | 2563 ---- .../src/i18n/locales/translations/fi-FI.json | 2563 ---- .../src/i18n/locales/translations/he-IL.json | 2563 ---- .../src/i18n/locales/translations/hi-IN.json | 2563 ---- .../src/i18n/locales/translations/hu-HU.json | 2563 ---- .../src/i18n/locales/translations/it-IT.json | 2563 ---- .../src/i18n/locales/translations/ms-MY.json | 2563 ---- .../src/i18n/locales/translations/no-NO.json | 2384 ---- .../src/i18n/locales/translations/pl-PL.json | 2563 ---- .../src/i18n/locales/translations/pt-BR.json | 6 +- .../src/i18n/locales/translations/ro-RO.json | 2384 ---- .../src/i18n/locales/translations/sl-SI.json | 2563 ---- .../src/i18n/locales/translations/sr-SP.json | 2563 ---- .../src/i18n/locales/translations/sv-SE.json | 2563 ---- .../src/i18n/locales/translations/sw-TZ.json | 2563 ---- .../src/i18n/locales/translations/uk-UA.json | 2563 ---- .../src/i18n/locales/translations/ur-PK.json | 2563 ---- packages/uniswap/src/react-native-dotenv.d.ts | 7 + .../src/state/uniswapMigrationTests.ts | 28 +- .../uniswap/src/state/uniswapMigrations.ts | 72 +- packages/uniswap/src/state/uniswapReducer.ts | 2 +- packages/uniswap/src/test/fixtures/account.ts | 19 + .../src/test/fixtures/transactions/swap.ts | 86 +- .../src/test/fixtures/wallet/addresses.ts | 1 + .../src/test/fixtures/wallet/currencies.ts | 1 - packages/uniswap/src/types/walletConnect.ts | 2 +- packages/uniswap/src/utils/currency.ts | 2 +- .../uniswapRoutingProvider.ts | 19 +- .../utils/routingDiagram/routingRegistry.ts | 7 +- packages/utilities/.gitignore | 3 +- packages/utilities/babel.config.js | 54 - .../src/async/retryWithBackoff.test.ts | 117 + .../utilities/src/async/retryWithBackoff.ts | 114 + .../src/device/locales.native.test.ts | 5 +- .../utilities/src/environment/env.native.ts | 7 +- packages/utilities/src/environment/env.web.ts | 5 +- .../src/environment/getCurrentEnv.ts | 17 + .../src/format/canonicalJson.test.ts | 56 + .../utilities/src/format/canonicalJson.ts | 57 + .../utilities/src/format/localeBased.test.ts | 36 + .../src/format/localeBasedFormats.ts | 115 +- packages/utilities/src/primitives/objects.ts | 11 +- packages/utilities/src/react/hooks.ts | 8 +- .../src/react/useDebouncedCallback.tsx | 4 +- .../src/react/useHasValueBecomeTruthy.test.ts | 177 + .../src/react/useHasValueBecomeTruthy.ts | 13 + .../src/react}/useInfiniteScroll.test.tsx | 32 +- .../utilities/src/react}/useInfiniteScroll.ts | 14 +- .../src/react/useThrottledCallback.tsx | 2 +- packages/utilities/src/reactQuery/cache.ts | 12 + .../utilities/src/telemetry/trace/utils.ts | 3 +- packages/utilities/src/theme/colors.ts | 13 + packages/utilities/src/time/timing.ts | 6 +- .../transactions/hexlifyTransaction.test.ts} | 31 +- .../src/transactions/hexlifyTransaction.ts | 36 + packages/wallet/.gitignore | 1 + packages/wallet/babel.config.js | 9 +- .../BatchedTransactionDetails.tsx | 6 +- .../WalletPreviewCard.test.tsx.snap | 623 +- .../AccountDetails.test.tsx.snap | 790 +- .../__snapshots__/LinkButton.test.tsx.snap | 227 +- .../dappRequests}/AccountSelectPopover.tsx | 24 +- .../src/components/dappRequests/AssetLogo.tsx | 61 + .../dappRequests/DappConnectionContent.tsx | 73 + .../DappConnectionPermissions.tsx | 82 +- .../dappRequests}/DappHeaderIcon.tsx | 15 +- .../dappRequests/DappPersonalSignContent.tsx | 97 + .../dappRequests/DappRequestFooter.tsx | 56 + .../dappRequests/DappRequestHeader.tsx | 47 + .../dappRequests/DappScanInfoModal.tsx | 80 + .../DappSendCallsScanningContent.tsx | 126 + .../dappRequests/DappSignTypedDataContent.tsx | 128 + .../DappTransactionScanningContent.tsx | 115 + .../dappRequests/DappWalletLineItem.tsx | 41 + .../SignTypedData}/DomainContent.tsx | 12 +- .../MaybeExplorerLinkedAddress.tsx | 3 +- .../NonStandardTypedDataContent.tsx | 67 + .../SignTypedData/Permit2Content.tsx | 94 + .../StandardTypedDataContent.tsx | 95 + .../dappRequests/SignatureMessageSection.tsx | 91 + .../TransactionApprovingSection.tsx | 161 + .../dappRequests/TransactionAssetList.tsx | 248 + .../dappRequests/TransactionErrorSection.tsx | 33 + .../dappRequests/TransactionLoadingState.tsx | 44 + .../dappRequests/TransactionPreviewCard.tsx | 166 + .../TransactionReceivingSection.tsx | 26 + .../TransactionRequestDetails.tsx | 132 + .../TransactionSendingSection.tsx | 26 + .../dappRequests/TransactionWarningBanner.tsx | 131 + .../hooks/useTypedDataWarningConfirmation.ts | 76 + .../dappRequests/types/EIP712Types.ts | 0 .../dappRequests/types/Permit2Types.ts | 2 +- .../src/components/introCards/IntroCard.tsx | 215 +- .../introCards/useSharedIntroCards.ts | 109 +- .../src/components/menu/ContextMenu.tsx | 68 +- .../PortfolioBalanceModal.tsx | 29 +- .../modals/SmartWalletActionRequiredModal.tsx | 20 +- .../modals/SmartWalletConfirmModal.tsx | 22 +- .../modals/SmartWalletCreatedModal.tsx | 5 +- .../modals/SmartWalletDisableWarningModal.tsx | 7 +- .../modals/SmartWalletEducationalModal.tsx | 5 +- .../modals/SmartWalletEnabledModal.tsx | 5 +- ...tWalletInsufficientFundsOnNetworkModal.tsx | 22 +- .../smartWallet/modals/SmartWalletModal.tsx | 76 +- .../smartWallet/modals/SmartWalletNudge.tsx | 6 +- .../modals/SmartWalletUnavailableModal.tsx | 5 +- .../modals/SmartWalletUpgradeModal.tsx | 10 +- .../data/apollo/usePersistedApolloClient.tsx | 4 +- packages/wallet/src/data/onRampAuthLink.ts | 51 - .../src/features/batchedTransactions/utils.ts | 35 +- .../src/features/behaviorHistory/selectors.ts | 9 + .../src/features/behaviorHistory/slice.ts | 17 + .../src/features/contracts/ContractManager.ts | 2 +- .../hooks/useBlockaidJsonRpcScan.ts | 53 + .../hooks/useBlockaidTransactionScan.test.ts | 270 + .../hooks/useBlockaidTransactionScan.ts | 53 + .../hooks/useBlockaidVerification.test.ts | 244 + .../hooks/useBlockaidVerification.ts | 33 + .../hooks/useDappConnectionConfirmation.ts | 59 + .../hooks/useParseUniswapXSwap.ts | 122 + .../hooks/useTypedDataSections.ts | 77 + .../wallet/src/features/dappRequests/types.ts | 85 +- .../dappRequests/utils/blockaidUtils.test.ts | 1072 ++ .../dappRequests/utils/blockaidUtils.ts | 426 + .../utils/buildBlockaidScanJsonRpcRequest.ts | 33 + .../buildBlockaidScanTransactionRequest.ts | 36 + .../dappRequests/utils/riskUtils.test.ts | 123 + .../features/dappRequests/utils/riskUtils.ts | 45 + .../dappRequests/verification.test.ts | 110 + .../src/features/dappRequests/verification.ts | 27 + ...SupportedNetworkNotification.test.tsx.snap | 384 +- .../smartWallet/hooks/useChainFiatFee.ts | 14 +- .../features/smartWallet/utils/gasFeeUtils.ts | 6 +- .../TransactionHistoryUpdater.tsx | 2 +- .../TransactionRequest/AddressFooter.tsx | 1 + .../TransactionRequest/NetworkFeeFooter.tsx | 4 +- .../TransactionHistoryUpdater.test.tsx.snap | 10 +- .../transactionServiceImpl.test.ts | 2 - .../transactionSignerService.ts | 7 - .../transactionSignerServiceImpl.test.ts | 73 - .../transactionSignerServiceImpl.ts | 35 +- .../signAndSubmitTransaction.ts | 2 +- .../features/transactions/rpcUtils.test.ts | 1 + .../transactions/swap/WalletSwapFlow.tsx | 12 +- .../transactions/swap/executeSwapSaga.test.ts | 1 - .../swap/hooks/useSwapCallback.ts | 104 - .../swap/hooks/useSwapHandlers.ts | 18 +- .../swap/hooks/useWrapCallback.ts | 21 - .../swap/prepareAndSignSwapSaga.test.ts | 1 - .../swap/prepareAndSignSwapSaga.ts | 20 +- .../swap/services/transactionParamsFactory.ts | 4 + .../transactions/swap/swapSaga.test.ts | 385 - .../features/transactions/swap/swapSaga.ts | 283 - .../transactions/swap/types/fixtures.ts | 3 +- .../transactions/swap/wrapSaga.test.ts | 80 - .../features/transactions/swap/wrapSaga.ts | 86 - .../wallet/src/features/transactions/utils.ts | 3 + .../utils/cleanTransactionGasFields.test.ts | 100 + .../utils/cleanTransactionGasFields.ts | 58 + .../watcher/transactionFinalizationSaga.ts | 2 + .../features/transactions/watcher/utils.ts | 19 + .../watcher/watchTransactionSaga.ts | 13 +- .../features/unitags/ChangeUnitagModal.tsx | 44 +- .../unitags/EditUnitagProfileContent.tsx | 4 +- .../src/features/wallet/Keyring/crypto.ts | 2 +- packages/wallet/src/state/walletMigrations.ts | 4 +- packages/wallet/src/test/README.md | 1 - packages/wallet/src/test/rpcUtilsFixtures.ts | 4 + packages/wallet/src/utils/password.ts | 1 + packages/wallet/src/utils/transaction.ts | 26 - patches/@expo%2Fcli@0.24.21.patch | 14 + patches/@expo%2Fconfig-plugins@10.1.1.patch | 37 + ...react-native%2Fgradle-plugin@0.79.5.patch} | 4 +- patches/@tamagui%2Fpopover@1.125.17.patch | 97 - patches/@tamagui%2Fportal@1.136.1.patch | 45 + patches/@tamagui%2Fstatic@1.136.1.patch | 152 + patches/@tamagui%2Fweb@1.125.17.patch | 66 - .../@tamagui%2Fz-index-stack@1.125.17.patch | 54 - patches/lightweight-charts@4.1.1.patch | 421 - patches/react-native-reanimated@3.16.7.patch | 30 - patches/react-native-reanimated@3.19.3.patch | 92 + scripts/check-bun-version.sh | 66 - scripts/check-runtime-versions.sh | 143 + .../src/generators/package/files/package.json | 4 +- 1713 files changed, 103713 insertions(+), 107445 deletions(-) create mode 100755 .claude/hooks/skill-activation-prompt.sh create mode 100644 .claude/hooks/skill-activation-prompt.ts create mode 100644 .claude/settings.json create mode 100644 .claude/skills/skill-rules.json create mode 100644 .claude/skills/web-e2e/SKILL.md create mode 100644 .cursor/cli.json create mode 100644 .cursorignore delete mode 100644 CODEOWNERS create mode 100644 apps/api-self-serve/.eslintrc.js create mode 100644 apps/api-self-serve/.gitignore create mode 100644 apps/api-self-serve/README.md create mode 100644 apps/api-self-serve/app/app.css create mode 100644 apps/api-self-serve/app/lib/utils.ts create mode 100644 apps/api-self-serve/app/root.tsx create mode 100644 apps/api-self-serve/app/routes.ts create mode 100644 apps/api-self-serve/app/routes/home.tsx create mode 100644 apps/api-self-serve/app/welcome/logo-dark.svg create mode 100644 apps/api-self-serve/app/welcome/logo-light.svg create mode 100644 apps/api-self-serve/app/welcome/welcome.tsx create mode 100644 apps/api-self-serve/components.json create mode 100644 apps/api-self-serve/package.json create mode 100644 apps/api-self-serve/react-router.config.ts create mode 100644 apps/api-self-serve/tailwind.config.ts create mode 100644 apps/api-self-serve/tsconfig.eslint.json create mode 100644 apps/api-self-serve/tsconfig.json create mode 100644 apps/api-self-serve/vite.config.ts create mode 100644 apps/cli/.eslintrc.cjs create mode 100644 apps/cli/README.md create mode 100644 apps/cli/package.json create mode 100644 apps/cli/project.json create mode 100644 apps/cli/src/cli-ui.tsx create mode 100644 apps/cli/src/cli.ts create mode 100644 apps/cli/src/core/data-collector.ts create mode 100644 apps/cli/src/core/orchestrator.ts create mode 100644 apps/cli/src/index.ts create mode 100644 apps/cli/src/lib/ai-provider-vercel.ts create mode 100644 apps/cli/src/lib/ai-provider.ts create mode 100644 apps/cli/src/lib/analysis-writer.ts create mode 100644 apps/cli/src/lib/cache-keys.ts create mode 100644 apps/cli/src/lib/cache-provider-sqlite.ts create mode 100644 apps/cli/src/lib/cache-provider.ts create mode 100644 apps/cli/src/lib/logger.ts create mode 100644 apps/cli/src/lib/pr-body-cleaner.ts create mode 100644 apps/cli/src/lib/release-scanner.ts create mode 100644 apps/cli/src/lib/stream-handler.ts create mode 100644 apps/cli/src/lib/team-members.ts create mode 100644 apps/cli/src/lib/team-resolver.ts create mode 100644 apps/cli/src/lib/trivial-files.ts create mode 100644 apps/cli/src/prompts/bug-bisect.md create mode 100644 apps/cli/src/prompts/release-changelog.md create mode 100644 apps/cli/src/prompts/team-digest.md create mode 100644 apps/cli/src/ui/App.tsx create mode 100644 apps/cli/src/ui/components/Banner.tsx create mode 100644 apps/cli/src/ui/components/Box.tsx create mode 100644 apps/cli/src/ui/components/ChangelogPreview.tsx create mode 100644 apps/cli/src/ui/components/FormField.tsx create mode 100644 apps/cli/src/ui/components/NumberInput.tsx create mode 100644 apps/cli/src/ui/components/ProgressIndicator.tsx create mode 100644 apps/cli/src/ui/components/ReleaseList.tsx create mode 100644 apps/cli/src/ui/components/Select.tsx create mode 100644 apps/cli/src/ui/components/StatusBadge.tsx create mode 100644 apps/cli/src/ui/components/TextInput.tsx create mode 100644 apps/cli/src/ui/components/Toggle.tsx create mode 100644 apps/cli/src/ui/components/WindowedSelect.tsx create mode 100644 apps/cli/src/ui/hooks/useAnalysis.ts create mode 100644 apps/cli/src/ui/hooks/useAppState.tsx create mode 100644 apps/cli/src/ui/hooks/useEditableField.ts create mode 100644 apps/cli/src/ui/hooks/useFormNavigation.ts create mode 100644 apps/cli/src/ui/hooks/useReleases.ts create mode 100644 apps/cli/src/ui/hooks/useRepository.ts create mode 100644 apps/cli/src/ui/hooks/useTeams.ts create mode 100644 apps/cli/src/ui/hooks/useToggleGroup.ts create mode 100644 apps/cli/src/ui/screens/BugBisectResultsScreen.tsx create mode 100644 apps/cli/src/ui/screens/BugInputScreen.tsx create mode 100644 apps/cli/src/ui/screens/ConfigReview.tsx create mode 100644 apps/cli/src/ui/screens/ExecutionScreen.tsx create mode 100644 apps/cli/src/ui/screens/ReleaseSelector.tsx create mode 100644 apps/cli/src/ui/screens/ResultsScreen.tsx create mode 100644 apps/cli/src/ui/screens/TeamDetailsScreen.tsx create mode 100644 apps/cli/src/ui/screens/TeamSelectorScreen.tsx create mode 100644 apps/cli/src/ui/screens/WelcomeScreen.tsx create mode 100644 apps/cli/src/ui/services/orchestrator-service.ts create mode 100644 apps/cli/src/ui/utils/colors.ts create mode 100644 apps/cli/src/ui/utils/format.ts create mode 100644 apps/cli/tsconfig.json create mode 100644 apps/cli/tsconfig.lint.json create mode 100644 apps/extension/src/app/features/dappRequests/requestContent/EthSend/ParsedTransaction/ParsedTransactionRequestContent.tsx delete mode 100644 apps/extension/src/app/features/dappRequests/requestContent/SignTypeData/Permit2/Permit2RequestContent.tsx create mode 100644 apps/extension/src/app/features/settings/password/ChangePasswordForm.test.tsx create mode 100644 apps/extension/src/app/features/settings/password/__snapshots__/ChangePasswordForm.test.tsx.snap create mode 100644 apps/mobile/.fingerprintignore create mode 100644 apps/mobile/android/app/src/main/java/com/uniswap/notifications/SilentPushEventEmitterModule.kt create mode 100644 apps/mobile/android/app/src/main/java/com/uniswap/notifications/SilentPushNotificationServiceExtension.kt create mode 100644 apps/mobile/android/app/src/main/java/com/uniswap/utils/JsonWritableExtensions.kt create mode 100644 apps/mobile/app.config.ts delete mode 100644 apps/mobile/app.json create mode 100644 apps/mobile/eas.json create mode 100644 apps/mobile/fingerprint.config.js delete mode 100644 apps/mobile/ios/Uniswap/AppDelegate.h delete mode 100644 apps/mobile/ios/Uniswap/AppDelegate.m create mode 100644 apps/mobile/ios/Uniswap/AppDelegate.swift create mode 100644 apps/mobile/ios/Uniswap/Notifications/SilentPushEventEmitter.m create mode 100644 apps/mobile/ios/Uniswap/Notifications/SilentPushEventEmitter.swift create mode 100644 apps/mobile/ios/Uniswap/Uniswap-Bridging-Header.h delete mode 100644 apps/mobile/ios/Uniswap/main.m delete mode 100644 apps/mobile/rnef.config.mjs create mode 100755 apps/mobile/scripts/check-android-gradle.sh create mode 100755 apps/mobile/scripts/check-podfile.sh create mode 100644 apps/mobile/scripts/getFingerprintForRadonIDE.ts create mode 100644 apps/mobile/src/app/navigation/constants.ts rename apps/mobile/src/app/navigation/tabs/{SwapLongPressModal.tsx => SwapLongPressOverlay.tsx} (54%) delete mode 100644 apps/mobile/src/components/explore/search/SearchPopularNFTCollections.graphql delete mode 100644 apps/mobile/src/components/icons/TripleDot.tsx create mode 100644 apps/mobile/src/components/modals/ReactNavigationModals/ReportTokenDataModalScreen.tsx create mode 100644 apps/mobile/src/components/modals/ReactNavigationModals/ReportTokenIssueModalScreen.tsx delete mode 100644 apps/mobile/src/features/deepLinking/handleInAppBrowserSaga.ts create mode 100644 apps/mobile/src/features/notifications/SilentPushListener.ts create mode 100644 apps/web/.storybook/__mocks__/tty.js create mode 100644 apps/web/public/images/notifications/monad_banner_light.png create mode 100644 apps/web/public/images/notifications/monad_logo_filled.png rename apps/web/public/{vercel-csp.json => staging-csp.json} (69%) create mode 100755 apps/web/scripts/start-anvil.sh create mode 100644 apps/web/src/appGraphql/data/apollo/retryLink.ts create mode 100644 apps/web/src/appGraphql/data/pools/usePoolData.test.ts create mode 100644 apps/web/src/assets/images/portfolio-connect-wallet-banner-grid/dark.svg create mode 100644 apps/web/src/assets/images/portfolio-connect-wallet-banner-grid/light.svg create mode 100644 apps/web/src/assets/images/portfolio-page-disconnected-preview/dark.svg create mode 100644 apps/web/src/assets/images/portfolio-page-disconnected-preview/light.svg create mode 100644 apps/web/src/assets/images/portfolio-page-disconnected-preview/mobile-dark.svg create mode 100644 apps/web/src/assets/images/portfolio-page-disconnected-preview/mobile-light.svg create mode 100644 apps/web/src/assets/svg/demo-wallet-emblem.svg delete mode 100644 apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/ActivityTab.anvil.e2e.test.ts create mode 100644 apps/web/src/components/AccountDrawer/MiniPortfolio/MiniPortfolioV2.tsx create mode 100644 apps/web/src/components/AccountDrawer/ReportedActivityToggle.tsx rename apps/web/src/components/AccountDrawer/{SpamToggle.tsx => SpamTokensToggle.tsx} (58%) create mode 100644 apps/web/src/components/ActionTiles/ActionTileWithIconAnimation.tsx create mode 100644 apps/web/src/components/ActionTiles/BuyActionTile.tsx create mode 100644 apps/web/src/components/ActionTiles/MoreActionTile.tsx create mode 100644 apps/web/src/components/ActionTiles/ReceiveActionTile.tsx create mode 100644 apps/web/src/components/ActionTiles/SendActionTile/SendActionTile.tsx rename apps/web/src/components/{AccountDrawer => ActionTiles/SendActionTile}/SendButtonTooltip.tsx (94%) create mode 100644 apps/web/src/components/ActionTiles/SwapActionTile.tsx delete mode 100644 apps/web/src/components/Banner/SolanaPromo/SolanaPromoBanner.tsx delete mode 100644 apps/web/src/components/Banner/SolanaPromo/SolanaPromoModal.tsx rename apps/web/src/components/Banner/shared/{Banners.tsx => OutageBanners.tsx} (100%) create mode 100644 apps/web/src/components/Charts/ChartTooltip.tsx create mode 100644 apps/web/src/components/Charts/CustomHoverMarker.tsx create mode 100644 apps/web/src/components/Charts/LiquidityChart/utils/calculateAnchoredLiquidityByTick.test.ts create mode 100644 apps/web/src/components/Charts/LiquidityChart/utils/calculateAnchoredLiquidityByTick.ts create mode 100644 apps/web/src/components/Charts/LiquidityChart/utils/calculateTokensLocked.ts create mode 100644 apps/web/src/components/Charts/LiquidityChart/utils/getAmounts.test.ts create mode 100644 apps/web/src/components/Charts/LiquidityChart/utils/getAmounts.ts create mode 100644 apps/web/src/components/Charts/LiveDotRenderer.tsx create mode 100644 apps/web/src/components/Charts/StaleBanner.tsx create mode 100644 apps/web/src/components/Charts/ToucanChart/renderer.tsx create mode 100644 apps/web/src/components/Charts/ToucanChart/toucan-chart-series.tsx create mode 100644 apps/web/src/components/Charts/hooks/useApplyChartTextureEffects.ts rename apps/web/src/components/Charts/{hooks.ts => hooks/useHeaderDateFormatter.ts} (100%) create mode 100644 apps/web/src/components/CurrencyInputPanel/SwapCurrencyInputPanel.test.tsx delete mode 100644 apps/web/src/components/Icons/CreditCard.tsx delete mode 100644 apps/web/src/components/Icons/Globe.tsx delete mode 100644 apps/web/src/components/Liquidity/LPIncentives/hooks/useLpIncentiveClaimMutation.ts create mode 100644 apps/web/src/components/Liquidity/ReviewModal.tsx create mode 100644 apps/web/src/components/Liquidity/hooks/useReportPositionHandler.ts create mode 100644 apps/web/src/components/NavBar/UniswapWrappedEntry.tsx create mode 100644 apps/web/src/components/Table/TableBody.tsx create mode 100644 apps/web/src/components/Table/TableRow.tsx create mode 100644 apps/web/src/components/Table/constants.ts create mode 100644 apps/web/src/components/Table/types.ts create mode 100644 apps/web/src/components/Table/utils/hasRow.ts delete mode 100644 apps/web/src/components/Tokens/TokenDetails/ChartSection/ChartTypeSelector.tsx create mode 100644 apps/web/src/components/Tokens/TokenDetails/ChartSection/ChartTypeToggle.tsx create mode 100644 apps/web/src/components/Tokens/TokenDetails/MoreButton.tsx create mode 100644 apps/web/src/components/TopLevelBanners/UniswapWrapped2025Banner.tsx create mode 100644 apps/web/src/components/Toucan/Auction/AuctionStats/AuctionStats.tsx create mode 100644 apps/web/src/components/Toucan/Auction/BidActivities/BidActivities.tsx create mode 100644 apps/web/src/components/Toucan/Auction/BidActivities/BidActivity.tsx create mode 100644 apps/web/src/components/Toucan/Auction/BidDistributionChart/BidDistributionChartFooter.tsx create mode 100644 apps/web/src/components/Toucan/Auction/BidDistributionChart/BidDistributionChartPlaceholder.tsx create mode 100644 apps/web/src/components/Toucan/Auction/BidDistributionChart/BidDistributionChartRenderer.tsx create mode 100644 apps/web/src/components/Toucan/Auction/BidDistributionChart/BlockUpdateCountdown.tsx create mode 100644 apps/web/src/components/Toucan/Auction/BidDistributionChart/constants.ts create mode 100644 apps/web/src/components/Toucan/Auction/BidDistributionChart/dev/CustomizePresetForm.tsx create mode 100644 apps/web/src/components/Toucan/Auction/BidDistributionChart/dev/MockDataSelectorModal.tsx create mode 100644 apps/web/src/components/Toucan/Auction/BidDistributionChart/dev/SavedPresetsList.tsx create mode 100644 apps/web/src/components/Toucan/Auction/BidDistributionChart/dev/customPresets.ts create mode 100644 apps/web/src/components/Toucan/Auction/BidDistributionChart/dev/devUtils.ts create mode 100644 apps/web/src/components/Toucan/Auction/BidDistributionChart/dev/useCustomPresetsStore.ts create mode 100644 apps/web/src/components/Toucan/Auction/BidDistributionChart/hooks/useChartDimensions.ts create mode 100644 apps/web/src/components/Toucan/Auction/BidDistributionChart/hooks/useChartLabels.ts create mode 100644 apps/web/src/components/Toucan/Auction/BidDistributionChart/hooks/useChartTooltip.ts create mode 100644 apps/web/src/components/Toucan/Auction/BidDistributionChart/utils/bidConcentration.ts create mode 100644 apps/web/src/components/Toucan/Auction/BidDistributionChart/utils/clearingPrice/label.ts create mode 100644 apps/web/src/components/Toucan/Auction/BidDistributionChart/utils/clearingPrice/position.test.ts create mode 100644 apps/web/src/components/Toucan/Auction/BidDistributionChart/utils/clearingPrice/position.ts create mode 100644 apps/web/src/components/Toucan/Auction/BidDistributionChart/utils/utils.ts create mode 100644 apps/web/src/components/Toucan/Auction/hooks/useAuctionBlockPolling.ts create mode 100644 apps/web/src/components/Toucan/Auction/hooks/useBidTokenInfo.ts create mode 100644 apps/web/src/components/Toucan/Auction/store/mocks/distributionData/100_Ticks.ts create mode 100644 apps/web/src/components/Toucan/Auction/store/mocks/distributionData/10_Ticks.ts create mode 100644 apps/web/src/components/Toucan/Auction/store/mocks/distributionData/20_Ticks.ts create mode 100644 apps/web/src/components/Toucan/Auction/store/mocks/distributionData/50_Ticks.ts create mode 100644 apps/web/src/components/Toucan/Auction/store/mocks/distributionData/bidDistributionMockData.ts create mode 100644 apps/web/src/components/Toucan/Auction/store/mocks/useMockDataStore.ts create mode 100644 apps/web/src/components/Toucan/Auction/utils/computeAuctionProgress.ts create mode 100644 apps/web/src/components/Toucan/TopAuctionsTable.tsx delete mode 100644 apps/web/src/featureFlags/flags/outageBanner.ts create mode 100644 apps/web/src/hooks/useBlockCountdown.test.ts create mode 100644 apps/web/src/hooks/useBlockCountdown.ts create mode 100644 apps/web/src/hooks/useChainOutageConfig.ts delete mode 100644 apps/web/src/hooks/useLpIncentivesClaim.ts create mode 100644 apps/web/src/hooks/useUpdateManualOutage.test.tsx create mode 100644 apps/web/src/hooks/useUpdateManualOutage.ts create mode 100644 apps/web/src/notification-service/WebNotificationService.tsx create mode 100644 apps/web/src/notification-service/createLocalStorageAdapter.ts create mode 100644 apps/web/src/notification-service/data-sources/createLegacyBannersNotificationDataSource.ts create mode 100644 apps/web/src/notification-service/notification-renderer/ModalNotification.tsx create mode 100644 apps/web/src/notification-service/notification-renderer/NotificationContainer.tsx create mode 100644 apps/web/src/notification-service/notification-renderer/StackedLowerLeftBanners.tsx create mode 100644 apps/web/src/notification-service/notification-renderer/createWebNotificationRenderer.ts create mode 100644 apps/web/src/notification-service/notification-renderer/notificationStore.ts create mode 100644 apps/web/src/notification-service/notification-renderer/stackingUtils.test.ts create mode 100644 apps/web/src/notification-service/notification-renderer/stackingUtils.ts create mode 100644 apps/web/src/notification-service/telemetry/getNotificationTelemetry.ts create mode 100644 apps/web/src/pages/CreatePosition/CreatePositionModal.test.tsx delete mode 100644 apps/web/src/pages/Explore/Toucan.tsx create mode 100644 apps/web/src/pages/Migrate/MigrateLiquidityTxContext.tsx create mode 100644 apps/web/src/pages/Migrate/MigrateV3.anvil.e2e.test.ts create mode 100644 apps/web/src/pages/Migrate/MigrateV3.e2e.test.ts rename apps/web/src/pages/{MigrateV3 => Migrate}/hooks/useInitialPosition.ts (100%) create mode 100644 apps/web/src/pages/Migrate/hooks/useMigrateLPPositionTxInfo.ts rename apps/web/src/pages/{MigrateV3 => Migrate}/index.tsx (79%) delete mode 100644 apps/web/src/pages/MigrateV3/MigrateV3.anvil.e2e.test.ts delete mode 100644 apps/web/src/pages/MigrateV3/MigrateV3.e2e.test.ts delete mode 100644 apps/web/src/pages/MigrateV3/MigrateV3LiquidityTxContext.tsx create mode 100644 apps/web/src/pages/Portfolio/Activity/ActivityFilters.tsx create mode 100644 apps/web/src/pages/Portfolio/Activity/ActivityTable/ActivityAddressCell.tsx create mode 100644 apps/web/src/pages/Portfolio/Activity/ActivityTable/ActivityAddressLookupStore.tsx create mode 100644 apps/web/src/pages/Portfolio/Activity/ActivityTable/ActivityAmountCell/ActivityAmountCell.tsx create mode 100644 apps/web/src/pages/Portfolio/Activity/ActivityTable/ActivityAmountCell/ApproveAmountCell.tsx create mode 100644 apps/web/src/pages/Portfolio/Activity/ActivityTable/ActivityAmountCell/CompactLayout.tsx create mode 100644 apps/web/src/pages/Portfolio/Activity/ActivityTable/ActivityAmountCell/DualTokenLayout.tsx create mode 100644 apps/web/src/pages/Portfolio/Activity/ActivityTable/ActivityAmountCell/EmptyCell.tsx create mode 100644 apps/web/src/pages/Portfolio/Activity/ActivityTable/ActivityAmountCell/utils.tsx create mode 100644 apps/web/src/pages/Portfolio/Activity/ActivityTable/ActivityTable.tsx create mode 100644 apps/web/src/pages/Portfolio/Activity/ActivityTable/AddressWithAvatar.tsx create mode 100644 apps/web/src/pages/Portfolio/Activity/ActivityTable/NftAmountDisplay.tsx create mode 100644 apps/web/src/pages/Portfolio/Activity/ActivityTable/TimeCell.tsx create mode 100644 apps/web/src/pages/Portfolio/Activity/ActivityTable/TokenAmountDisplay.tsx create mode 100644 apps/web/src/pages/Portfolio/Activity/ActivityTable/TransactionTypeCell.tsx create mode 100644 apps/web/src/pages/Portfolio/Activity/ActivityTable/activityTableModels.ts create mode 100644 apps/web/src/pages/Portfolio/Activity/ActivityTable/registry.ts create mode 100644 apps/web/src/pages/Portfolio/Activity/PaginationSkeletonRow.tsx create mode 100644 apps/web/src/pages/Portfolio/ConnectWalletFixedBottomButton.tsx create mode 100644 apps/web/src/pages/Portfolio/EmptyTableCell.tsx delete mode 100644 apps/web/src/pages/Portfolio/Header/hooks/usePortfolioParams.ts create mode 100644 apps/web/src/pages/Portfolio/Header/hooks/usePortfolioRoutes.ts create mode 100644 apps/web/src/pages/Portfolio/Header/useShouldHeaderBeCompact.tsx create mode 100644 apps/web/src/pages/Portfolio/NFTs/NFTCardSkeleton.tsx create mode 100644 apps/web/src/pages/Portfolio/NFTs/generateRotationStyle.ts delete mode 100644 apps/web/src/pages/Portfolio/Overview.tsx create mode 100644 apps/web/src/pages/Portfolio/Overview/ActionTiles.tsx create mode 100644 apps/web/src/pages/Portfolio/Overview/MiniActivityTable.tsx create mode 100644 apps/web/src/pages/Portfolio/Overview/MiniPoolsTable/MiniPoolsTable.tsx create mode 100644 apps/web/src/pages/Portfolio/Overview/MiniPoolsTable/columns/Balance.tsx create mode 100644 apps/web/src/pages/Portfolio/Overview/MiniPoolsTable/columns/Fees.tsx create mode 100644 apps/web/src/pages/Portfolio/Overview/MiniPoolsTable/columns/Info.tsx create mode 100644 apps/web/src/pages/Portfolio/Overview/MiniPoolsTable/columns/Status.tsx create mode 100644 apps/web/src/pages/Portfolio/Overview/MiniTokensTable.tsx create mode 100644 apps/web/src/pages/Portfolio/Overview/OpenLimitsTable.tsx create mode 100644 apps/web/src/pages/Portfolio/Overview/Overview.tsx create mode 100644 apps/web/src/pages/Portfolio/Overview/OverviewTables.tsx create mode 100644 apps/web/src/pages/Portfolio/Overview/PortfolioChart.tsx create mode 100644 apps/web/src/pages/Portfolio/Overview/StatsTiles.tsx create mode 100644 apps/web/src/pages/Portfolio/Overview/TableSectionHeader.tsx create mode 100644 apps/web/src/pages/Portfolio/Overview/ViewAllButton.tsx create mode 100644 apps/web/src/pages/Portfolio/Overview/constants.ts create mode 100644 apps/web/src/pages/Portfolio/Overview/hooks/useIsPortfolioZero.ts create mode 100644 apps/web/src/pages/Portfolio/Overview/hooks/useSwapUSDValuesFromGraphQL.ts create mode 100644 apps/web/src/pages/Portfolio/Overview/hooks/useSwapsThisWeek.ts create mode 100644 apps/web/src/pages/Portfolio/Overview/utils/checkBalanceDiffWithinRange.ts create mode 100644 apps/web/src/pages/Portfolio/PortfolioPageInner.tsx create mode 100644 apps/web/src/pages/Portfolio/Tokens/Table/columns/Price.tsx create mode 100644 apps/web/src/pages/Portfolio/Tokens/Table/columns/useTokenColumns.tsx create mode 100644 apps/web/src/pages/Portfolio/Tokens/hooks/useNavigateToTokenDetails.ts create mode 100644 apps/web/src/pages/Portfolio/components/AnimatedStyledBanner/AnimatedEmblems.tsx create mode 100644 apps/web/src/pages/Portfolio/components/AnimatedStyledBanner/AnimatedStyledBanner.tsx create mode 100644 apps/web/src/pages/Portfolio/components/AnimatedStyledBanner/Emblems/EmblemA.tsx create mode 100644 apps/web/src/pages/Portfolio/components/AnimatedStyledBanner/Emblems/EmblemB.tsx create mode 100644 apps/web/src/pages/Portfolio/components/AnimatedStyledBanner/Emblems/EmblemC.tsx create mode 100644 apps/web/src/pages/Portfolio/components/AnimatedStyledBanner/Emblems/EmblemD.tsx create mode 100644 apps/web/src/pages/Portfolio/components/AnimatedStyledBanner/Emblems/EmblemE.tsx create mode 100644 apps/web/src/pages/Portfolio/components/AnimatedStyledBanner/Emblems/EmblemF.tsx create mode 100644 apps/web/src/pages/Portfolio/components/AnimatedStyledBanner/Emblems/EmblemG.tsx create mode 100644 apps/web/src/pages/Portfolio/components/AnimatedStyledBanner/Emblems/EmblemH.tsx create mode 100644 apps/web/src/pages/Portfolio/components/AnimatedStyledBanner/Emblems/index.ts create mode 100644 apps/web/src/pages/Portfolio/components/AnimatedStyledBanner/Emblems/types.ts create mode 100644 apps/web/src/pages/Portfolio/components/PortfolioExpandoRow.tsx create mode 100644 apps/web/src/pages/Portfolio/constants.ts create mode 100644 apps/web/src/pages/Portfolio/hooks/usePortfolioAddresses.ts create mode 100644 apps/web/src/pages/Portfolio/utils/portfolioUrls.ts create mode 100644 apps/web/src/pages/Wrapped/DisconnectedState.tsx create mode 100644 apps/web/src/pages/Wrapped/index.tsx create mode 100644 apps/web/src/pages/getPortfolioTitle.ts create mode 100644 apps/web/src/playwright/fixtures/urls.test.ts create mode 100644 apps/web/src/playwright/fixtures/urls.ts create mode 100644 apps/web/src/playwright/mocks/dataApiService/get_portfolio.json create mode 100644 apps/web/src/playwright/mocks/fiatOnRamp/get-country.json create mode 100644 apps/web/src/playwright/mocks/fiatOnRamp/quotes.json create mode 100644 apps/web/src/playwright/mocks/fiatOnRamp/supported-countries.json create mode 100644 apps/web/src/playwright/mocks/fiatOnRamp/supported-fiat-currencies.json create mode 100644 apps/web/src/playwright/mocks/fiatOnRamp/supported-tokens.json create mode 100644 apps/web/src/playwright/mocks/graphql/Token/uni_token_price.json create mode 100644 apps/web/src/playwright/mocks/tradingApi/swap.json create mode 100644 apps/web/src/state/explore/topAuctions.ts create mode 100644 apps/web/src/state/migrations/58.test.ts create mode 100644 apps/web/src/state/migrations/58.ts create mode 100644 apps/web/src/state/migrations/59.test.ts create mode 100644 apps/web/src/state/migrations/59.ts create mode 100644 apps/web/src/state/migrations/60.test.ts create mode 100644 apps/web/src/state/migrations/60.ts create mode 100644 apps/web/src/state/outage/atoms.ts create mode 100644 apps/web/src/state/outage/types.ts create mode 100644 apps/web/src/state/sagas/transactions/useSwapHandlers.test.ts create mode 100644 apps/web/src/state/sagas/transactions/useSwapHandlers.ts create mode 100644 apps/web/src/test-utils/mockTamagui.ts create mode 100644 apps/web/src/utils/errors/isOutageError.ts create mode 100644 apps/web/src/utils/filterDefinedWalletAddresses.ts create mode 100644 apps/web/vite/entry-gateway-proxy.ts create mode 100644 packages/api/scripts/fixGraphQLApiTypes.mts create mode 100644 packages/api/src/clients/blockaid/createBlockaidApiClient.ts create mode 100644 packages/api/src/clients/blockaid/types.ts create mode 100644 packages/api/src/clients/graphql/generated.ts create mode 100644 packages/api/src/clients/liquidity/createLiquidityServiceClient.ts create mode 100644 packages/api/src/components/ApiInit.test.tsx create mode 100644 packages/api/src/components/ApiInit.tsx create mode 100644 packages/api/src/getEntryGatewayUrl.ts delete mode 100644 packages/api/src/getIsSessionServiceEnabled.ts delete mode 100644 packages/api/src/getSessionService.native.ts delete mode 100644 packages/api/src/getSessionService.ts delete mode 100644 packages/api/src/getSessionService.web.ts create mode 100644 packages/api/src/provideDeviceIdService.ts create mode 100644 packages/api/src/provideSessionService.native.ts create mode 100644 packages/api/src/provideSessionService.ts create mode 100644 packages/api/src/provideSessionService.web.ts create mode 100644 packages/api/src/provideSessionStorage.ts create mode 100644 packages/api/src/provideUniswapIdentifierService.ts create mode 100644 packages/api/src/storage/createExtensionStorageDriver.ts create mode 100644 packages/api/src/storage/createNativeStorageDriver.ts create mode 100644 packages/api/src/storage/createWebStorageDriver.ts create mode 100644 packages/api/src/storage/getStorageDriver.native.ts create mode 100644 packages/api/src/storage/getStorageDriver.ts create mode 100644 packages/api/src/storage/getStorageDriver.web.ts create mode 100644 packages/api/src/storage/types.ts create mode 100644 packages/api/src/transport.ts create mode 100644 packages/biome-config/project.json create mode 100644 packages/biome-config/src/extractor.test.js create mode 100644 packages/biome-config/src/fixtures/array-merge-config.jsonc create mode 100644 packages/biome-config/src/fixtures/no-markers-config.jsonc create mode 100644 packages/biome-config/src/fixtures/off-override-config.jsonc create mode 100644 packages/biome-config/src/fixtures/simple-config.jsonc create mode 100644 packages/biome-config/src/merger.test.js create mode 100644 packages/biome-config/src/processor.test.js create mode 100644 packages/biome-config/src/universePackages.js create mode 100644 packages/eslint-config/plugins/no-transform-percentage-strings.js create mode 100644 packages/eslint-config/plugins/no-transform-percentage-strings.test.js create mode 100644 packages/gating/src/getIsSessionServiceEnabled.ts create mode 100644 packages/gating/src/getIsSessionUpgradeAutoEnabled.ts create mode 100644 packages/notifications/src/getIsNotificationServiceEnabled.ts create mode 100644 packages/notifications/src/global.d.ts create mode 100644 packages/notifications/src/notification-data-source/NotificationDataSource.ts create mode 100644 packages/notifications/src/notification-data-source/getNotificationQueryOptions.ts create mode 100644 packages/notifications/src/notification-data-source/implementations/createNotificationDataSource.ts create mode 100644 packages/notifications/src/notification-data-source/implementations/createPollingNotificationDataSource.test.ts create mode 100644 packages/notifications/src/notification-data-source/implementations/createPollingNotificationDataSource.ts create mode 100644 packages/notifications/src/notification-processor/NotificationProcessor.ts create mode 100644 packages/notifications/src/notification-processor/implementations/createBaseNotificationProcessor.test.ts create mode 100644 packages/notifications/src/notification-processor/implementations/createBaseNotificationProcessor.ts create mode 100644 packages/notifications/src/notification-processor/implementations/createNotificationProcessor.test.ts create mode 100644 packages/notifications/src/notification-processor/implementations/createNotificationProcessor.ts create mode 100644 packages/notifications/src/notification-renderer/NotificationRenderer.ts create mode 100644 packages/notifications/src/notification-renderer/components/BannerTemplate.tsx create mode 100644 packages/notifications/src/notification-renderer/components/InlineBannerNotification.tsx create mode 100644 packages/notifications/src/notification-renderer/implementations/createNotificationRenderer.ts create mode 100644 packages/notifications/src/notification-service/NotificationService.ts create mode 100644 packages/notifications/src/notification-service/implementations/createNotificationService.test.ts create mode 100644 packages/notifications/src/notification-service/implementations/createNotificationService.ts create mode 100644 packages/notifications/src/notification-telemetry/NotificationTelemetry.ts create mode 100644 packages/notifications/src/notification-telemetry/implementations/createNotificationTelemetry.test.ts create mode 100644 packages/notifications/src/notification-telemetry/implementations/createNotificationTelemetry.ts create mode 100644 packages/notifications/src/notification-tracker/NotificationTracker.ts create mode 100644 packages/notifications/src/notification-tracker/implementations/createApiNotificationTracker.test.ts create mode 100644 packages/notifications/src/notification-tracker/implementations/createApiNotificationTracker.ts create mode 100644 packages/notifications/src/notification-tracker/implementations/createNoopNotificationTracker.ts create mode 100644 packages/notifications/src/notification-tracker/implementations/createNotificationTracker.ts create mode 100644 packages/notifications/src/utils/formatNotificationType.test.ts create mode 100644 packages/notifications/src/utils/formatNotificationType.ts create mode 100644 packages/notifications/vitest-setup.ts create mode 100644 packages/notifications/vitest.config.ts create mode 100644 packages/sessions/src/challenge-solvers/createChallengeSolverService.ts create mode 100644 packages/sessions/src/challenge-solvers/createHashcashMockSolver.ts create mode 100644 packages/sessions/src/challenge-solvers/createHashcashSolver.test.ts create mode 100644 packages/sessions/src/challenge-solvers/createHashcashSolver.ts create mode 100644 packages/sessions/src/challenge-solvers/createNoneMockSolver.ts create mode 100644 packages/sessions/src/challenge-solvers/createTurnstileMockSolver.ts create mode 100644 packages/sessions/src/challenge-solvers/createTurnstileSolver.ts create mode 100644 packages/sessions/src/challenge-solvers/hashcash/core.test.ts create mode 100644 packages/sessions/src/challenge-solvers/hashcash/core.ts create mode 100644 packages/sessions/src/challenge-solvers/turnstileSolver.integration.test.ts create mode 100644 packages/sessions/src/challenge-solvers/types.ts create mode 100644 packages/sessions/src/challengeFlow.integration.test.ts create mode 100644 packages/sessions/src/session-initialization/createSessionInitializationService.test.ts create mode 100644 packages/sessions/src/session-initialization/createSessionInitializationService.ts create mode 100644 packages/sessions/src/session-initialization/sessionErrors.ts delete mode 100644 packages/sessions/src/session-repository/transport.ts create mode 100644 packages/sessions/src/session.integration.test.ts create mode 100644 packages/sessions/src/sessionLifecycle.integration.test.ts create mode 100644 packages/sessions/src/test-utils.ts create mode 100644 packages/sessions/src/test-utils/createLocalCookieTransport.ts create mode 100644 packages/sessions/src/test-utils/mocks.ts create mode 100644 packages/sessions/src/uniswap-identifier/createUniswapIdentifierService.ts create mode 100644 packages/sessions/src/uniswap-identifier/types.ts create mode 100644 packages/ui/src/assets/backgrounds/dots-banner-dark.png create mode 100644 packages/ui/src/assets/backgrounds/dots-banner-light.png create mode 100644 packages/ui/src/assets/backgrounds/monad-test-banner-light.png create mode 100644 packages/ui/src/assets/graphics/bridged-assets-v2-card-banner-dark.png create mode 100644 packages/ui/src/assets/graphics/bridged-assets-v2-card-banner-light.png create mode 100644 packages/ui/src/assets/graphics/zero-percent.png create mode 100644 packages/ui/src/assets/icons/approve-alt.svg create mode 100644 packages/ui/src/assets/icons/avatar-placeholder.svg create mode 100644 packages/ui/src/assets/icons/box.svg create mode 100644 packages/ui/src/assets/icons/chart-bar-crossed.svg create mode 100644 packages/ui/src/assets/icons/credit-card.svg create mode 100644 packages/ui/src/assets/icons/crosschain-icon.svg create mode 100644 packages/ui/src/assets/icons/eth-mini.svg create mode 100644 packages/ui/src/assets/icons/gift.svg create mode 100644 packages/ui/src/assets/icons/money-hand.svg create mode 100644 packages/ui/src/assets/icons/receipt.svg create mode 100644 packages/ui/src/assets/icons/send-alt.svg create mode 100644 packages/ui/src/assets/icons/shield-magnifying-glass.svg create mode 100644 packages/ui/src/assets/icons/snowflake.svg create mode 100644 packages/ui/src/assets/logos/png/monad-logo-filled.png delete mode 100644 packages/ui/src/assets/logos/png/monad-logo.png create mode 100644 packages/ui/src/components/RefreshButton/RefreshButton.native.tsx create mode 100644 packages/ui/src/components/RefreshButton/RefreshButton.tsx create mode 100644 packages/ui/src/components/RefreshButton/RefreshButton.web.tsx create mode 100644 packages/ui/src/components/RefreshButton/RefreshButtonIcon.tsx create mode 100644 packages/ui/src/components/icons/ApproveAlt.tsx create mode 100644 packages/ui/src/components/icons/AvatarPlaceholder.tsx create mode 100644 packages/ui/src/components/icons/Box.tsx create mode 100644 packages/ui/src/components/icons/ChartBarCrossed.tsx create mode 100644 packages/ui/src/components/icons/CreditCard.tsx create mode 100644 packages/ui/src/components/icons/CrosschainIcon.tsx create mode 100644 packages/ui/src/components/icons/EthMini.tsx create mode 100644 packages/ui/src/components/icons/Gift.tsx create mode 100644 packages/ui/src/components/icons/MoneyHand.tsx create mode 100644 packages/ui/src/components/icons/Receipt.tsx create mode 100644 packages/ui/src/components/icons/SendAlt.tsx create mode 100644 packages/ui/src/components/icons/ShieldMagnifyingGlass.tsx create mode 100644 packages/ui/src/components/icons/Snowflake.tsx create mode 100644 packages/ui/src/components/lines/VerticalDottedLineSeparator.tsx create mode 100644 packages/uniswap/src/components/ConfirmSwapModal/steps/SwapTXPlanStepRow.tsx create mode 100644 packages/uniswap/src/components/ConfirmSwapModal/useSecondsUntilDeadline.tsx create mode 100644 packages/uniswap/src/components/TokenSelector/UnsupportedChainedActionsBanner.tsx create mode 100644 packages/uniswap/src/components/activity/ActivityListEmptyState.tsx create mode 100644 packages/uniswap/src/components/banners/UniswapWrapped2025Banner/UniswapWrapped2025Banner.native.tsx create mode 100644 packages/uniswap/src/components/banners/UniswapWrapped2025Banner/UniswapWrapped2025Banner.tsx create mode 100644 packages/uniswap/src/components/banners/UniswapWrapped2025Banner/UniswapWrapped2025Banner.web.tsx create mode 100644 packages/uniswap/src/components/banners/UniswapWrapped2025Banner/types.ts create mode 100644 packages/uniswap/src/components/banners/UniswapWrapped2025Card/UniswapWrapped2025Card.native.tsx create mode 100644 packages/uniswap/src/components/banners/UniswapWrapped2025Card/UniswapWrapped2025Card.tsx create mode 100644 packages/uniswap/src/components/banners/UniswapWrapped2025Card/UniswapWrapped2025Card.web.tsx create mode 100644 packages/uniswap/src/components/banners/UniswapWrapped2025Card/types.ts create mode 100644 packages/uniswap/src/components/banners/shared/SharedSnowflakeComponents.tsx create mode 100644 packages/uniswap/src/components/banners/shared/utils.ts rename {apps/mobile/src/components/icons => packages/uniswap/src/components/chains}/BlockExplorerIcon.tsx (95%) create mode 100644 packages/uniswap/src/components/dialog/DialogButtons.tsx create mode 100644 packages/uniswap/src/components/dialog/DialogContent.tsx create mode 100644 packages/uniswap/src/components/dialog/GetHelpButtonUI.tsx create mode 100644 packages/uniswap/src/components/dialog/GetHelpHeader.native.tsx create mode 100644 packages/uniswap/src/components/dialog/GetHelpHeader.web.tsx create mode 100644 packages/uniswap/src/components/dialog/GetHelpHeaderContent.tsx create mode 100644 packages/uniswap/src/components/dialog/hooks/useDialogVisibility.test.ts create mode 100644 packages/uniswap/src/components/dialog/hooks/useDialogVisibility.ts create mode 100644 packages/uniswap/src/components/logos/PoweredByBlockaid.tsx create mode 100644 packages/uniswap/src/components/menus/hooks/useContextMenuTracking.ts create mode 100644 packages/uniswap/src/components/nfts/NftsListEmptyState.tsx create mode 100644 packages/uniswap/src/components/nfts/NftsListHeader.tsx create mode 100644 packages/uniswap/src/components/nfts/types.ts create mode 100644 packages/uniswap/src/components/notifications/ModalTemplate.tsx create mode 100644 packages/uniswap/src/components/notifications/MonadAnnouncementModal.tsx create mode 100644 packages/uniswap/src/components/reporting/ReportModal.tsx create mode 100644 packages/uniswap/src/components/reporting/ReportPoolDataModal.tsx create mode 100644 packages/uniswap/src/components/reporting/ReportTokenDataModal.tsx create mode 100644 packages/uniswap/src/components/reporting/ReportTokenIssueModal.tsx create mode 100644 packages/uniswap/src/components/reporting/input.tsx create mode 100644 packages/uniswap/src/components/tokens/TokensListEmptyState.tsx create mode 100644 packages/uniswap/src/data/apiClients/blockaidApi/BlockaidApiClient.ts create mode 100644 packages/uniswap/src/data/apiClients/liquidityService/LiquidityServiceClient.ts create mode 100644 packages/uniswap/src/data/apiClients/liquidityService/useMigrateV2ToV3LPPositionQuery.ts create mode 100644 packages/uniswap/src/data/apiClients/liquidityService/useMigrateV3ToV4LPPositionQuery.ts create mode 100644 packages/uniswap/src/data/rest/auctions/auctionService.ts create mode 100644 packages/uniswap/src/data/rest/auctions/paths.ts create mode 100644 packages/uniswap/src/data/rest/auctions/types.ts create mode 100644 packages/uniswap/src/data/rest/auctions/useGetAuctionDetailsQuery.ts create mode 100644 packages/uniswap/src/data/rest/auctions/useGetAuctionsQuery.ts create mode 100644 packages/uniswap/src/data/rest/auctions/useGetBidConcentrationQuery.ts create mode 100644 packages/uniswap/src/data/rest/auctions/useGetBidsByWalletInfiniteQuery.ts create mode 100644 packages/uniswap/src/data/rest/auctions/useGetBidsByWalletQuery.ts create mode 100644 packages/uniswap/src/data/rest/auctions/useGetLatestCheckpointQuery.ts create mode 100644 packages/uniswap/src/data/rest/getPortfolioChart.ts create mode 100644 packages/uniswap/src/dialog-preferences/DialogPreferencesService.ts create mode 100644 packages/uniswap/src/dialog-preferences/implementations/createDialogPreferencesService.test.ts create mode 100644 packages/uniswap/src/dialog-preferences/implementations/createDialogPreferencesService.ts create mode 100644 packages/uniswap/src/dialog-preferences/index.ts create mode 100644 packages/uniswap/src/dialog-preferences/types.ts create mode 100644 packages/uniswap/src/features/activity/hooks/useActivityData.test.tsx create mode 100644 packages/uniswap/src/features/activity/hooks/useFormattedTransactionDataForActivity.test.ts create mode 100644 packages/uniswap/src/features/activity/hooks/useMergeLocalAndRemoteTransactions.test.ts create mode 100644 packages/uniswap/src/features/activity/utils/extractDappInfo.ts create mode 100644 packages/uniswap/src/features/dataApi/tokenDetails/useTokenDetailsData.ts create mode 100644 packages/uniswap/src/features/nfts/utils.test.ts delete mode 100644 packages/uniswap/src/features/portfolio/PortfolioBalance/RefreshBalanceButton.tsx delete mode 100644 packages/uniswap/src/features/portfolio/portfolioUpdates/isInstantTokenBalanceUpdateEnabled.ts create mode 100644 packages/uniswap/src/features/reporting/reports.ts create mode 100644 packages/uniswap/src/features/search/SearchModal/hooks/useSectionsForNoQuerySearch.tsx create mode 100644 packages/uniswap/src/features/search/SearchModal/hooks/useWebSearchTabs.ts rename packages/uniswap/src/features/tokens/{ => warnings}/TokenWarningCard.tsx (95%) rename packages/uniswap/src/features/tokens/{ => warnings}/TokenWarningFlagsTable.tsx (95%) rename packages/uniswap/src/features/tokens/{ => warnings}/TokenWarningModal.tsx (80%) rename packages/uniswap/src/features/tokens/{ => warnings}/WarningInfoModalContainer.tsx (80%) rename packages/uniswap/src/features/tokens/{ => warnings}/hooks/useBlockaidFeeComparisonAnalytics.ts (96%) create mode 100644 packages/uniswap/src/features/tokens/warnings/hooks/useWarningModalCurrenciesDismissed.ts rename packages/uniswap/src/features/tokens/{ => warnings}/safetyUtils.test.ts (99%) rename packages/uniswap/src/features/tokens/{ => warnings}/safetyUtils.ts (97%) rename packages/uniswap/src/features/tokens/{ => warnings}/slice/hooks.ts (73%) rename packages/uniswap/src/features/tokens/{ => warnings}/slice/selectors.ts (58%) rename packages/uniswap/src/features/tokens/{ => warnings}/slice/slice.ts (66%) rename packages/uniswap/src/features/tokens/{ => warnings}/slice/types.ts (64%) create mode 100644 packages/uniswap/src/features/tokens/warnings/types.ts delete mode 100644 packages/uniswap/src/features/transactions/swap/components/SwapFormButton/hooks/useInterfaceWrap.ts create mode 100644 packages/uniswap/src/features/transactions/swap/form/SwapFormScreen/SwapFormScreenDetails/SwapFormScreenFooter/GasAndWarningRows/TradeInfoRow/GasInfoRow.test.tsx create mode 100644 packages/uniswap/src/features/transactions/swap/hooks/useSwapWarnings/getAztecUnavailableWarning.ts create mode 100644 packages/uniswap/src/features/transactions/swap/plan/types.ts create mode 100644 packages/uniswap/src/features/transactions/swap/review/SwapReviewScreen/SwapReviewFooter/DelayedSubmissionText.tsx create mode 100644 packages/uniswap/src/features/transactions/swap/review/SwapReviewScreen/SwapReviewFooter/PendingSwapButton.tsx create mode 100644 packages/uniswap/src/features/transactions/swap/utils/chainedActions.ts create mode 100644 packages/uniswap/src/features/transactions/swap/utils/getIdForQuote.ts create mode 100644 packages/uniswap/src/features/visibility/hooks/useIsActivityHidden.ts create mode 100644 packages/uniswap/src/features/visibility/hooks/useTokenVisibility.ts create mode 100644 packages/uniswap/src/hooks/useShouldShowAztecWarning.ts create mode 100644 packages/uniswap/src/hooks/useSnowflakeAnimation.ts delete mode 100644 packages/uniswap/src/i18n/locales/translations/af-ZA.json delete mode 100644 packages/uniswap/src/i18n/locales/translations/ar-SA.json delete mode 100644 packages/uniswap/src/i18n/locales/translations/ca-ES.json delete mode 100644 packages/uniswap/src/i18n/locales/translations/cs-CZ.json delete mode 100644 packages/uniswap/src/i18n/locales/translations/da-DK.json delete mode 100644 packages/uniswap/src/i18n/locales/translations/de-DE.json delete mode 100644 packages/uniswap/src/i18n/locales/translations/el-GR.json delete mode 100644 packages/uniswap/src/i18n/locales/translations/fi-FI.json delete mode 100644 packages/uniswap/src/i18n/locales/translations/he-IL.json delete mode 100644 packages/uniswap/src/i18n/locales/translations/hi-IN.json delete mode 100644 packages/uniswap/src/i18n/locales/translations/hu-HU.json delete mode 100644 packages/uniswap/src/i18n/locales/translations/it-IT.json delete mode 100644 packages/uniswap/src/i18n/locales/translations/ms-MY.json delete mode 100644 packages/uniswap/src/i18n/locales/translations/no-NO.json delete mode 100644 packages/uniswap/src/i18n/locales/translations/pl-PL.json delete mode 100644 packages/uniswap/src/i18n/locales/translations/ro-RO.json delete mode 100644 packages/uniswap/src/i18n/locales/translations/sl-SI.json delete mode 100644 packages/uniswap/src/i18n/locales/translations/sr-SP.json delete mode 100644 packages/uniswap/src/i18n/locales/translations/sv-SE.json delete mode 100644 packages/uniswap/src/i18n/locales/translations/sw-TZ.json delete mode 100644 packages/uniswap/src/i18n/locales/translations/uk-UA.json delete mode 100644 packages/uniswap/src/i18n/locales/translations/ur-PK.json create mode 100644 packages/uniswap/src/test/fixtures/account.ts create mode 100644 packages/uniswap/src/test/fixtures/wallet/addresses.ts delete mode 100644 packages/utilities/babel.config.js create mode 100644 packages/utilities/src/async/retryWithBackoff.test.ts create mode 100644 packages/utilities/src/async/retryWithBackoff.ts create mode 100644 packages/utilities/src/environment/getCurrentEnv.ts create mode 100644 packages/utilities/src/format/canonicalJson.test.ts create mode 100644 packages/utilities/src/format/canonicalJson.ts create mode 100644 packages/utilities/src/react/useHasValueBecomeTruthy.test.ts create mode 100644 packages/utilities/src/react/useHasValueBecomeTruthy.ts rename {apps/web/src/hooks => packages/utilities/src/react}/useInfiniteScroll.test.tsx (76%) rename {apps/web/src/hooks => packages/utilities/src/react}/useInfiniteScroll.ts (74%) rename packages/{wallet/src/utils/transaction.test.ts => utilities/src/transactions/hexlifyTransaction.test.ts} (57%) create mode 100644 packages/utilities/src/transactions/hexlifyTransaction.ts rename {apps/mobile/src/components/Requests/ScanSheet => packages/wallet/src/components/dappRequests}/AccountSelectPopover.tsx (88%) create mode 100644 packages/wallet/src/components/dappRequests/AssetLogo.tsx create mode 100644 packages/wallet/src/components/dappRequests/DappConnectionContent.tsx rename apps/mobile/src/components/Requests/ScanSheet/SitePermissions.tsx => packages/wallet/src/components/dappRequests/DappConnectionPermissions.tsx (62%) rename {apps/mobile/src/components/Requests => packages/wallet/src/components/dappRequests}/DappHeaderIcon.tsx (77%) create mode 100644 packages/wallet/src/components/dappRequests/DappPersonalSignContent.tsx create mode 100644 packages/wallet/src/components/dappRequests/DappRequestFooter.tsx create mode 100644 packages/wallet/src/components/dappRequests/DappRequestHeader.tsx create mode 100644 packages/wallet/src/components/dappRequests/DappScanInfoModal.tsx create mode 100644 packages/wallet/src/components/dappRequests/DappSendCallsScanningContent.tsx create mode 100644 packages/wallet/src/components/dappRequests/DappSignTypedDataContent.tsx create mode 100644 packages/wallet/src/components/dappRequests/DappTransactionScanningContent.tsx create mode 100644 packages/wallet/src/components/dappRequests/DappWalletLineItem.tsx rename {apps/extension/src/app/features/dappRequests/requestContent/SignTypeData => packages/wallet/src/components/dappRequests/SignTypedData}/DomainContent.tsx (84%) rename {apps/extension/src/app/features/dappRequests/requestContent/SignTypeData => packages/wallet/src/components/dappRequests/SignTypedData}/MaybeExplorerLinkedAddress.tsx (87%) create mode 100644 packages/wallet/src/components/dappRequests/SignTypedData/NonStandardTypedDataContent.tsx create mode 100644 packages/wallet/src/components/dappRequests/SignTypedData/Permit2Content.tsx create mode 100644 packages/wallet/src/components/dappRequests/SignTypedData/StandardTypedDataContent.tsx create mode 100644 packages/wallet/src/components/dappRequests/SignatureMessageSection.tsx create mode 100644 packages/wallet/src/components/dappRequests/TransactionApprovingSection.tsx create mode 100644 packages/wallet/src/components/dappRequests/TransactionAssetList.tsx create mode 100644 packages/wallet/src/components/dappRequests/TransactionErrorSection.tsx create mode 100644 packages/wallet/src/components/dappRequests/TransactionLoadingState.tsx create mode 100644 packages/wallet/src/components/dappRequests/TransactionPreviewCard.tsx create mode 100644 packages/wallet/src/components/dappRequests/TransactionReceivingSection.tsx create mode 100644 packages/wallet/src/components/dappRequests/TransactionRequestDetails.tsx create mode 100644 packages/wallet/src/components/dappRequests/TransactionSendingSection.tsx create mode 100644 packages/wallet/src/components/dappRequests/TransactionWarningBanner.tsx create mode 100644 packages/wallet/src/components/dappRequests/hooks/useTypedDataWarningConfirmation.ts rename {apps/extension/src/app/features => packages/wallet/src/components}/dappRequests/types/EIP712Types.ts (100%) rename {apps/extension/src/app/features => packages/wallet/src/components}/dappRequests/types/Permit2Types.ts (96%) delete mode 100644 packages/wallet/src/data/onRampAuthLink.ts create mode 100644 packages/wallet/src/features/dappRequests/hooks/useBlockaidJsonRpcScan.ts create mode 100644 packages/wallet/src/features/dappRequests/hooks/useBlockaidTransactionScan.test.ts create mode 100644 packages/wallet/src/features/dappRequests/hooks/useBlockaidTransactionScan.ts create mode 100644 packages/wallet/src/features/dappRequests/hooks/useBlockaidVerification.test.ts create mode 100644 packages/wallet/src/features/dappRequests/hooks/useBlockaidVerification.ts create mode 100644 packages/wallet/src/features/dappRequests/hooks/useDappConnectionConfirmation.ts create mode 100644 packages/wallet/src/features/dappRequests/hooks/useParseUniswapXSwap.ts create mode 100644 packages/wallet/src/features/dappRequests/hooks/useTypedDataSections.ts create mode 100644 packages/wallet/src/features/dappRequests/utils/blockaidUtils.test.ts create mode 100644 packages/wallet/src/features/dappRequests/utils/blockaidUtils.ts create mode 100644 packages/wallet/src/features/dappRequests/utils/buildBlockaidScanJsonRpcRequest.ts create mode 100644 packages/wallet/src/features/dappRequests/utils/buildBlockaidScanTransactionRequest.ts create mode 100644 packages/wallet/src/features/dappRequests/utils/riskUtils.test.ts create mode 100644 packages/wallet/src/features/dappRequests/utils/riskUtils.ts create mode 100644 packages/wallet/src/features/dappRequests/verification.test.ts create mode 100644 packages/wallet/src/features/dappRequests/verification.ts delete mode 100644 packages/wallet/src/features/transactions/swap/hooks/useSwapCallback.ts delete mode 100644 packages/wallet/src/features/transactions/swap/hooks/useWrapCallback.ts delete mode 100644 packages/wallet/src/features/transactions/swap/swapSaga.test.ts delete mode 100644 packages/wallet/src/features/transactions/swap/swapSaga.ts delete mode 100644 packages/wallet/src/features/transactions/swap/wrapSaga.test.ts delete mode 100644 packages/wallet/src/features/transactions/swap/wrapSaga.ts create mode 100644 packages/wallet/src/features/transactions/utils/cleanTransactionGasFields.test.ts create mode 100644 packages/wallet/src/features/transactions/utils/cleanTransactionGasFields.ts create mode 100644 packages/wallet/src/features/transactions/watcher/utils.ts delete mode 100644 packages/wallet/src/utils/transaction.ts create mode 100644 patches/@expo%2Fcli@0.24.21.patch create mode 100644 patches/@expo%2Fconfig-plugins@10.1.1.patch rename patches/{@react-native%2Fgradle-plugin@0.77.2.patch => @react-native%2Fgradle-plugin@0.79.5.patch} (82%) delete mode 100644 patches/@tamagui%2Fpopover@1.125.17.patch create mode 100644 patches/@tamagui%2Fportal@1.136.1.patch create mode 100644 patches/@tamagui%2Fstatic@1.136.1.patch delete mode 100644 patches/@tamagui%2Fweb@1.125.17.patch delete mode 100644 patches/@tamagui%2Fz-index-stack@1.125.17.patch delete mode 100644 patches/lightweight-charts@4.1.1.patch delete mode 100644 patches/react-native-reanimated@3.16.7.patch create mode 100644 patches/react-native-reanimated@3.19.3.patch delete mode 100755 scripts/check-bun-version.sh create mode 100755 scripts/check-runtime-versions.sh diff --git a/.bun-version b/.bun-version index 9728bd69ac8..3a3cd8cc8b0 100644 --- a/.bun-version +++ b/.bun-version @@ -1 +1 @@ -1.2.21 +1.3.1 diff --git a/.claude/hooks/skill-activation-prompt.sh b/.claude/hooks/skill-activation-prompt.sh new file mode 100755 index 00000000000..27fb24d0584 --- /dev/null +++ b/.claude/hooks/skill-activation-prompt.sh @@ -0,0 +1,6 @@ +#!/bin/bash +# Copied from https://github.com/diet103/claude-code-infrastructure-showcase/blob/c586f9d8854989abbe9040cde61527888ded3904/.claude/hooks/skill-activation-prompt.sh +set -e + +cd "$CLAUDE_PROJECT_DIR/.claude/hooks" +cat | bun run skill-activation-prompt.ts diff --git a/.claude/hooks/skill-activation-prompt.ts b/.claude/hooks/skill-activation-prompt.ts new file mode 100644 index 00000000000..f8feef6343d --- /dev/null +++ b/.claude/hooks/skill-activation-prompt.ts @@ -0,0 +1,132 @@ +#!/usr/bin/env node +/** biome-ignore-all lint/suspicious/noConsole: script output */ +// Copied from https://github.com/diet103/claude-code-infrastructure-showcase/blob/c586f9d8854989abbe9040cde61527888ded3904/.claude/hooks/skill-activation-prompt.ts +import { readFileSync } from 'fs' +import { join } from 'path' + +interface HookInput { + session_id: string + transcript_path: string + cwd: string + permission_mode: string + prompt: string +} + +interface PromptTriggers { + keywords?: string[] + intentPatterns?: string[] +} + +interface SkillRule { + type: 'guardrail' | 'domain' + enforcement: 'block' | 'suggest' | 'warn' + priority: 'critical' | 'high' | 'medium' | 'low' + promptTriggers?: PromptTriggers +} + +interface SkillRules { + version: string + skills: Record +} + +interface MatchedSkill { + name: string + matchType: 'keyword' | 'intent' + config: SkillRule +} + +async function main() { + try { + // Read input from stdin + const input = readFileSync(0, 'utf-8') + const data: HookInput = JSON.parse(input) + const prompt = data.prompt.toLowerCase() + + // Load skill rules + const projectDir = process.env.CLAUDE_PROJECT_DIR || '$HOME/project' + const rulesPath = join(projectDir, '.claude', 'skills', 'skill-rules.json') + const rules: SkillRules = JSON.parse(readFileSync(rulesPath, 'utf-8')) + + const matchedSkills: MatchedSkill[] = [] + + // Check each skill for matches + for (const [skillName, config] of Object.entries(rules.skills)) { + const triggers = config.promptTriggers + if (!triggers) { + continue + } + + // Keyword matching + if (triggers.keywords) { + const keywordMatch = triggers.keywords.some((kw) => prompt.includes(kw.toLowerCase())) + if (keywordMatch) { + matchedSkills.push({ name: skillName, matchType: 'keyword', config }) + continue + } + } + + // Intent pattern matching + if (triggers.intentPatterns) { + const intentMatch = triggers.intentPatterns.some((pattern) => { + const regex = new RegExp(pattern, 'i') + return regex.test(prompt) + }) + if (intentMatch) { + matchedSkills.push({ name: skillName, matchType: 'intent', config }) + } + } + } + + // Generate output if matches found + if (matchedSkills.length > 0) { + let output = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n' + output += '🎯 SKILL ACTIVATION CHECK\n' + output += '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n' + + // Group by priority + const critical = matchedSkills.filter((s) => s.config.priority === 'critical') + const high = matchedSkills.filter((s) => s.config.priority === 'high') + const medium = matchedSkills.filter((s) => s.config.priority === 'medium') + const low = matchedSkills.filter((s) => s.config.priority === 'low') + + if (critical.length > 0) { + output += '⚠️ CRITICAL SKILLS (REQUIRED):\n' + critical.forEach((s) => (output += ` → ${s.name}\n`)) + output += '\n' + } + + if (high.length > 0) { + output += '📚 RECOMMENDED SKILLS:\n' + high.forEach((s) => (output += ` → ${s.name}\n`)) + output += '\n' + } + + if (medium.length > 0) { + output += '💡 SUGGESTED SKILLS:\n' + medium.forEach((s) => (output += ` → ${s.name}\n`)) + output += '\n' + } + + if (low.length > 0) { + output += '📌 OPTIONAL SKILLS:\n' + low.forEach((s) => (output += ` → ${s.name}\n`)) + output += '\n' + } + + output += 'ACTION: Use Skill tool BEFORE responding\n' + output += '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n' + + console.log(output) + } + + process.exit(0) + } catch (err) { + console.error('Error in skill-activation-prompt hook:', err) + process.exit(1) + } +} + +main().catch((err) => { + console.error('Uncaught error:', err) + process.exit(1) +}) diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 00000000000..98c9f2f7c89 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,32 @@ +{ + "permissions": { + "deny": [ + "Read(**/.env)", + "Edit(**/.env)", + "Read(~/.aws/**)", + "Edit(~/.aws/**)", + "Read(~/.ssh/**)", + "Edit(~/.ssh/**)", + "Read(~/.gnupg/**)", + "Edit(~/.gnupg/**)", + "Read(~/.git-credentials)", + "Edit(~/.git-credentials)", + "Read($HOME/Library/Keychains/**)", + "Edit($HOME/Library/Keychains/**)", + "Read(/private/etc/**)", + "Edit(/private/etc/**)" + ] + }, + "hooks": { + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/skill-activation-prompt.sh" + } + ] + } + ] + } +} diff --git a/.claude/skills/skill-rules.json b/.claude/skills/skill-rules.json new file mode 100644 index 00000000000..83577579083 --- /dev/null +++ b/.claude/skills/skill-rules.json @@ -0,0 +1,29 @@ +{ + "version": "1.0", + "description": "Skill activation triggers for Claude Code. Controls when skills automatically suggest or block actions.", + "skills": { + "web-e2e": { + "type": "domain", + "enforcement": "block", + "priority": "critical", + "description": "Run, debug, and create Playwright e2e tests for the web app.", + "promptTriggers": { + "keywords": ["e2e", "end-to-end", "playwright"], + "intentPatterns": ["(run|start|debug|create|explain).*?e2e"] + } + } + }, + "notes": { + "enforcement_types": { + "suggest": "Skill suggestion appears but doesn't block execution", + "block": "Requires skill to be used before proceeding (guardrail)", + "warn": "Shows warning but allows proceeding" + }, + "priority_levels": { + "critical": "Highest - Always trigger when matched", + "high": "Important - Trigger for most matches", + "medium": "Moderate - Trigger for clear matches", + "low": "Optional - Trigger only for explicit matches" + } + } +} diff --git a/.claude/skills/web-e2e/SKILL.md b/.claude/skills/web-e2e/SKILL.md new file mode 100644 index 00000000000..aad00d76d9b --- /dev/null +++ b/.claude/skills/web-e2e/SKILL.md @@ -0,0 +1,350 @@ +--- +name: web-e2e +description: Run, create, and debug Playwright e2e tests for the web app. ALWAYS invoke this skill using the SlashCommand tool (i.e., `/web-e2e`) BEFORE attempting to run any e2e tests, playwright tests, anvil tests, or debug test failures. DO NOT run `bun playwright test` or other e2e commands directly - you must invoke this skill first to learn the correct commands and test architecture. +allowed-tools: [Read, Write, Edit, Bash, Glob, Grep, mcp__playwright__browser_navigate, mcp__playwright__browser_snapshot, mcp__playwright__browser_click, mcp__playwright__browser_type, mcp__playwright__browser_take_screenshot, mcp__playwright__browser_console_messages, mcp__playwright__browser_network_requests, mcp__playwright__browser_evaluate] +--- + +# Web E2E Testing Skill + +This skill helps you create and run end-to-end (e2e) Playwright tests for the Uniswap web application. + +## Test Architecture + +### Test Location +- All e2e tests live in `apps/web/src/` directory structure +- Test files use the naming convention: `*.e2e.test.ts` +- Anvil-specific tests (requiring local blockchain): `*.anvil.e2e.test.ts` + +### Automatic Wallet Connection + +**Important**: When running Playwright tests, the app automatically connects to a test wallet: +- **Address**: `0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266` (constant: `TEST_WALLET_ADDRESS`) +- **Display name**: `test0` (the Unitag associated with this address) +- **Connection**: Happens automatically via `wagmiAutoConnect.ts` when in Playwright environment + +This means: +- Tests start with a wallet already connected +- You can immediately test wallet-dependent features +- The wallet button will show "test0" instead of "Connect wallet" + +**When using Playwright MCP**: To enable automatic wallet connection when browsing via MCP tools, set the environment variable `REACT_APP_IS_PLAYWRIGHT_ENV=true` before starting the dev server. This makes the app behave identically to how it does in automated tests, with the test wallet auto-connected. + +### Custom Fixtures + +The web app uses custom Playwright fixtures and mocks that extend base Playwright functionality. +They are located in `apps/web/src/playwright/fixtures/*` and `apps/web/src/playwright/mocks/*`. + +#### Import Pattern +```typescript +import { expect, getTest } from 'playwright/fixtures' + +// For regular tests (no blockchain) +const test = getTest() + +// For anvil tests (with blockchain) +const test = getTest({ withAnvil: true }) +``` + +#### Available Fixtures + +1. **graphql** - Mock GraphQL responses + ```typescript + await graphql.intercept('OperationName', Mocks.Path.to_mock) + await graphql.waitForResponse('OperationName') + ``` + +2. **anvil** - Local blockchain client (only in anvil tests) + ```typescript + // Set token balances + await anvil.setErc20Balance({ address, balance }) + + // Check balances + await anvil.getBalance({ address }) + await anvil.getErc20Balance(tokenAddress, ownerAddress) + + // Manage allowances + await anvil.setErc20Allowance({ address, spender, amount }) + await anvil.setPermit2Allowance({ token, spender, amount }) + + // Mining blocks + await anvil.mine({ blocks: 1 }) + + // Snapshots for isolation + const snapshotId = await anvil.takeSnapshot() + await anvil.revertToSnapshot(snapshotId) + ``` + +3. **tradingApi** - Mock Trading API responses + ```typescript + await stubTradingApiEndpoint({ + page, + endpoint: uniswapUrls.tradingApiPaths.swap + }) + ``` + +4. **amplitude** - Analytics mocking (automatic) + +### Test Structure + +```typescript +import { expect, getTest } from 'playwright/fixtures' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' + +const test = getTest({ withAnvil: true }) // or getTest() for non-anvil + +test.describe('Feature Name', () => { + test.beforeEach(async ({ page }) => { + // Setup before each test + }) + + test('should do something', async ({ page, anvil, graphql }) => { + // Setup mocks + await graphql.intercept('Operation', Mocks.Path.mock) + + // Setup blockchain state (if anvil test) + await anvil.setErc20Balance({ address, balance }) + + // Navigate to page + await page.goto('/path') + + // Interact with UI using TestIDs + await page.getByTestId(TestID.SomeButton).click() + + // Make assertions + await expect(page.getByText('Expected Text')).toBeVisible() + }) +}) +``` + +### Best Practices + +1. **Use TestIDs** - Always use the TestID enum for selectors (not string literals) + ```typescript + // Good + await page.getByTestId(TestID.ReviewSwap) + + // Bad + await page.getByTestId('review-swap') + ``` + +2. **Mock External Services** - Use fixtures to mock GraphQL, Trading API, REST API etc. + ```typescript + await graphql.intercept('PortfolioBalances', Mocks.PortfolioBalances.test_wallet) + await stubTradingApiEndpoint({ page, endpoint: uniswapUrls.tradingApiPaths.quote }) + ``` + +3. **Use Mocks Helper** - Import mock paths from `playwright/mocks/mocks.ts` + ```typescript + import { Mocks } from 'playwright/mocks/mocks' + await graphql.intercept('Token', Mocks.Token.uni_token) + ``` + +4. **Test Constants** - Use constants from the codebase + ```typescript + import { USDT, DAI } from 'uniswap/src/constants/tokens' + import { TEST_WALLET_ADDRESS } from 'playwright/fixtures/wallets' + + // TEST_WALLET_ADDRESS is the automatically connected wallet + // It displays as "test0" in the UI + ``` + +5. **Anvil State Management** - Set up blockchain state properly + ```typescript + // Always set token balances before testing swaps + await anvil.setErc20Balance({ + address: assume0xAddress(USDT.address), + balance: 100_000_000n + }) + ``` + +## Running Tests + +The following commands must be run from the `apps/web/` folder. + +**⚠️ PREREQUISITE**: Playwright tests require the Vite preview server to be running at `http://localhost:3000` BEFORE tests start. The `bun e2e` commands handle this automatically, but if running tests directly you must start the server first. + +### Development Commands + +The `e2e` commands handle all requisite setup tasks for the playwright tests. These include building the app for production and running the Vite preview server. + +```bash +# Run all e2e tests (starts anvil, builds, and runs tests) +bun e2e + +# Run only non-anvil tests (faster, no blockchain required) +bun e2e:no-anvil + +# Run only anvil tests (blockchain tests only) +bun e2e:anvil + +# Run specific test file +bun e2e TokenSelector.e2e.test +``` + +### Direct Playwright Commands + +In some cases it may be helpful to run the commands more directly with the different tasks in different terminals. + +```bash +# Step 1: Build the web app for e2e +bun build:e2e + +# Step 2: Start the Vite preview server (REQUIRED - must be running before tests) +bun preview:e2e +# Wait for "Local: http://localhost:3000" message + +# (Optional) Step 3: Start Anvil (note, Anvil tests can start this themselves) +bun anvil:mainnet +# Wait for "Listening on 127.0.0.1:8545" message + +# Step 4: Run the playwright tests (only after servers are ready) +bun playwright:test +``` + +### Test Modes + +```bash +# Headed mode (see browser) +bun playwright test --headed + +# Debug mode with Playwright Inspector +bun playwright test --debug + +# UI mode (interactive) +bun playwright test --ui +``` + +## Configuration + +### Playwright Config (`playwright.config.ts`) + +Key settings: +- `testDir`: `./src` +- `testMatch`: `**/*.e2e.test.ts` +- `workers`: 1 (configured in CI) +- `fullyParallel`: false +- `baseURL`: `http://localhost:3000` + +## Common Patterns + +### Navigation and URL Testing +```typescript +await page.goto('/swap?inputCurrency=ETH&outputCurrency=USDT') +await expect(page.getByTestId(TestID.ChooseInputToken + '-label')).toHaveText('ETH') +``` + +### Form Interactions +```typescript +await page.getByTestId(TestID.AmountInputIn).fill('0.01') +await page.getByTestId(TestID.AmountInputIn).clear() +``` + +### Token Selection +```typescript +await page.getByTestId(TestID.ChooseOutputToken).click() +await page.getByTestId('token-option-1-USDT').first().click() +``` + +### Waiting for Transaction Completion +```typescript +await page.getByTestId(TestID.Swap).click() +await expect(page.getByText('Swapped')).toBeVisible() +``` + +### Blockchain Verification +```typescript +const balance = await anvil.getBalance({ address: TEST_WALLET_ADDRESS }) +await expect(balance).toBeLessThan(parseEther('10000')) +``` + +## Troubleshooting + +### Tests Timeout +- Check if Anvil is running: `bun anvil:mainnet` +- Ensure preview server is running: `bun preview:e2e` + +### Anvil Issues +- Tests automatically manage Anvil snapshots for isolation +- Anvil restarts automatically if unhealthy +- For manual restart: stop the e2e command and run again + +### Mock Not Working +- Ensure mock path is correct in `Mocks` object +- Check GraphQL operation name matches exactly +- Verify timing - intercept before the request is made + +### Test Flakiness +- Use proper waiting: `await expect(element).toBeVisible()` +- Don't use fixed `setTimeout` - use Playwright's auto-waiting +- Check for race conditions with network requests + +### Debugging + +- Run tests with `--headed` flag to watch the browser +- Use `--debug` flag to step through with Playwright Inspector +- Add `await page.pause()` in your test to stop at a specific point +- Check test output and error messages carefully +- Review screenshots/videos in `test-results/` directory after failures + +## Playwright Documentation References + +For more details on Playwright features, refer to: + +- **[Writing Tests](https://playwright.dev/docs/writing-tests)** - Test structure, actions, assertions +- **[Test Fixtures](https://playwright.dev/docs/test-fixtures)** - Creating custom fixtures (like our anvil/graphql fixtures) +- **[Running Tests](https://playwright.dev/docs/running-tests)** - Command line options, filtering, debugging +- **[API Testing](https://playwright.dev/docs/api-testing)** - Mocking and intercepting network requests +- **[Locators](https://playwright.dev/docs/locators)** - Finding elements (we use `getByTestId` primarily) +- **[Assertions](https://playwright.dev/docs/test-assertions)** - Available expect matchers +- **[Test Hooks](https://playwright.dev/docs/api/class-test#test-before-each)** - beforeEach, afterEach, beforeAll, afterAll +- **[Test Configuration](https://playwright.dev/docs/test-configuration)** - playwright.config.ts options +- **[Debugging Tests](https://playwright.dev/docs/debug)** - UI mode, inspector, trace viewer + +## Playwright MCP Integration (Optional but Recommended) + +The Playwright MCP (Model Context Protocol) provides browser automation capabilities that make test development and debugging easier: +- **Interactive debugging** - Navigate the app in a real browser to understand behavior +- **Creating tests** - Explore the UI to identify selectors and interactions +- **Debugging failures** - Inspect page state when tests fail + +### Installing Playwright MCP + +If you don't have the Playwright MCP installed, you can add it to your Claude Code configuration: + +1. Open Claude Code settings (Command/Ctrl + Shift + P → "Claude Code: Open Settings") +2. Add the Playwright MCP to your `mcpServers` configuration: + +```json +{ + "mcpServers": { + "playwright": { + "command": "npx", + "args": ["-y", "@executeautomation/playwright-mcp-server"] + } + } +} +``` + +3. Restart Claude Code + +Alternatively, follow the installation guide at: https://github.com/executeautomation/playwright-mcp + +### Using Playwright MCP for Test Development (Optional) + +**If you have the MCP installed**, you can use these tools during development: + +1. **Navigate and explore** - Use `mcp__playwright__browser_navigate` to visit pages +2. **Take snapshots** - Use `mcp__playwright__browser_snapshot` to see the page structure and find TestIDs +3. **Interact with elements** - Use `mcp__playwright__browser_click` and `mcp__playwright__browser_type` to test interactions +4. **Inspect state** - Use `mcp__playwright__browser_console_messages` and `mcp__playwright__browser_network_requests` to debug +5. **Take screenshots** - Use `mcp__playwright__browser_take_screenshot` to visualize issues + +## When to Use This Skill + +Use this skill when you need to: +- Create new end-to-end tests for web features +- Debug or fix failing e2e tests +- Run e2e tests during development +- Understand the e2e testing architecture +- Set up test fixtures or mocks +- Work with Anvil blockchain state in tests diff --git a/.cursor/cli.json b/.cursor/cli.json new file mode 100644 index 00000000000..b50608cd785 --- /dev/null +++ b/.cursor/cli.json @@ -0,0 +1,21 @@ +{ + "version": 1, + "permissions": { + "deny": [ + "Read(**/.env)", + "Write(**/.env)", + "Read(~/.aws/**)", + "Write(~/.aws/**)", + "Read(~/.ssh/**)", + "Write(~/.ssh/**)", + "Read(~/.gnupg/**)", + "Write(~/.gnupg/**)", + "Read(~/.git-credentials)", + "Write(~/.git-credentials)", + "Read($HOME/Library/Keychains/**)", + "Write($HOME/Library/Keychains/**)", + "Read(/private/etc/**)", + "Write(/private/etc/**)" + ] + } + } diff --git a/.cursorignore b/.cursorignore new file mode 100644 index 00000000000..70179d19f2e --- /dev/null +++ b/.cursorignore @@ -0,0 +1,7 @@ +**/.env +**/.aws/** +**/.ssh/** +**/.gnupg/** +**/.git-credentials +**/Library/Keychains/** +**/private/etc/** diff --git a/.env.defaults b/.env.defaults index af00c667e56..d46cd294f96 100644 --- a/.env.defaults +++ b/.env.defaults @@ -24,6 +24,9 @@ TRADING_API_KEY=stored-in-.env.local FIREBASE_APP_CHECK_DEBUG_TOKEN=stored-in-.env.local INCLUDE_PROTOTYPE_FEATURES=stored-in-.env.local IS_E2E_TEST=false +ENABLE_SESSION_SERVICE=false +ENABLE_SESSION_UPGRADE_AUTO=false +ENABLE_ENTRY_GATEWAY_PROXY=false # URL overrides (keep empty in this file) AMPLITUDE_PROXY_URL_OVERRIDE= API_BASE_URL_OVERRIDE= @@ -34,5 +37,9 @@ SCANTASTIC_API_URL_OVERRIDE= STATSIG_PROXY_URL_OVERRIDE= TRADING_API_URL_OVERRIDE= UNITAGS_API_URL_OVERRIDE= +UNISWAP_NOTIF_API_BASE_URL_OVERRIDE= +ENTRY_GATEWAY_API_URL_OVERRIDE= +LIQUIDITY_SERVICE_URL_OVERRIDE= GH_TOKEN_RN_CLI= JUPITER_PROXY_URL= +BLOCKAID_PROXY_URL= diff --git a/.gitignore b/.gitignore index b3c573b208b..d284a0e6cc0 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ yarn-error.log* # local env files .env +.env.local .env.defaults.local # turbo @@ -50,6 +51,9 @@ packages/uniswap/src/i18n/locales/source/*_old.json # ci .ci-cache/ +# cli gh analysis artifacts +.analysis/ + # JetBrains .idea/ @@ -59,8 +63,11 @@ packages/uniswap/src/i18n/locales/source/*_old.json # CodeTours Extension .tours/* -# RNEF -.rnef/ +# Expo +.expo/ + +# auto-generated test ids +apps/mobile/.maestro/scripts/testIds.js # claude claude.local.md diff --git a/.nxignore b/.nxignore index 3e785b63ed7..52d9b176674 100644 --- a/.nxignore +++ b/.nxignore @@ -10,6 +10,7 @@ apps/extension/dev apps/extension/build packages/*/dist packages/*/types +dist/out-tsc # Ignore Generator Templates tools/**/generators/**/files diff --git a/CLAUDE.md b/CLAUDE.md index 00a8280afa0..7224bbdb032 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -44,6 +44,7 @@ bun extension build:production # Extension production ```bash bun g:test # Run all tests +bun notifications test # Run tests for a specific package (e.g. notifications) bun g:test:coverage # With coverage bun web playwright:test # Web E2E tests bun mobile e2e # Mobile E2E tests @@ -158,4 +159,4 @@ Be cognizant of the app or package within which a given change is being made. Be - If the user needs help with an Nx configuration or project graph error, use the `nx_workspace` tool to get any errors - \ No newline at end of file + diff --git a/CODEOWNERS b/CODEOWNERS deleted file mode 100644 index f70773659eb..00000000000 --- a/CODEOWNERS +++ /dev/null @@ -1 +0,0 @@ -* @uniswap/web-admins diff --git a/RELEASE b/RELEASE index 12680356c4b..a302b26e29b 100644 --- a/RELEASE +++ b/RELEASE @@ -1,63 +1,63 @@ -IPFS hash of the deployment: -- CIDv0: `QmUF5sYAgjigJ6D7nE6p12SG1rFrwYLidKxeX52qBN1Pce` -- CIDv1: `bafybeicxxeljdmh6f2s3xo43piks2h7ibgm6hbfoo3ftc5wjfmqfsngghm` - -The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org). - -You can also access the Uniswap Interface from an IPFS gateway. -**BEWARE**: The Uniswap interface uses [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to remember your settings, such as which tokens you have imported. -**You should always use an IPFS gateway that enforces origin separation**, or our hosted deployment of the latest release at [app.uniswap.org](https://app.uniswap.org). -Your Uniswap settings are never remembered across different URLs. - -IPFS gateways: -- https://bafybeicxxeljdmh6f2s3xo43piks2h7ibgm6hbfoo3ftc5wjfmqfsngghm.ipfs.dweb.link/ -- [ipfs://QmUF5sYAgjigJ6D7nE6p12SG1rFrwYLidKxeX52qBN1Pce/](ipfs://QmUF5sYAgjigJ6D7nE6p12SG1rFrwYLidKxeX52qBN1Pce/) - -## 5.116.0 (2025-10-28) - - -### Features - -* **web:** add activity table to the tab with real data (#23506) f00228c -* **web:** Add createRejectableMockConnector util to force tx rejection (#24574) 3b3b2b7 -* **web:** add demo account support for activity tab (#24639) 9ec0194 -* **web:** add disconnected portfolio view (#23690) 7a1b085 -* **web:** add fiat to price chart (#23577) fab99ce -* **web:** add hidden tokens table rows (#23535) 291fab3 -* **web:** add loading state to tokens table (#23544) ed5ced8 -* **web:** add more & better filtering + transaction parsing (#24579) 205c03d -* **web:** add v2 bridged asset banner (#24734) 4666868 -* **web:** disconnected view B version (#24630) 46ca828 -* **web:** Help Modal styling nits (#24547) ae252e6 -* **web:** NFTs tab (#23604) a438b54 -* **web:** small style nits for Company menu (#24318) 4d71e08 -* **web:** special case metamask dual vm connection flow (#24756) faabc72 -* **web:** tokens table search (#23509) b83fc75 -* **web:** update CompanyMenu arrangement on tablet width (#24312) 758f68d - - -### Bug Fixes - -* **web:** default to mainnet for limits flow [STAGING] (#24885) 5a8e150 -* **web:** Fix CreatePosition e2e anvil test (#24573) d68b011 -* **web:** Fix e2e anvil tests missing quote stub (#24590) 838d5bd -* **web:** Fix limit order chain switch bug (#23064) b11176d -* **web:** Fix Swap e2e anvil tests (#24662) 26adf5c -* **web:** fixes pools tab loader skeletons (#24472) 2f887aa -* **web:** Increase anvil manager timeout (#24623) 466eb69 -* **web:** log interface swap finalization results for flashblocks (#24869) bf30270 -* **web:** support chain filtering query params (#24754) 4bc3729 -* **web:** update the create flow to display the latest dependnet amount (#24676) 168c20a -* **web:** Use Mainnet instead of Base for e2e test commands (#24589) ff7dfee - - -### Continuous Integration - -* **web:** update sitemaps 4e8124b - - -### Tests - -* **web:** Disable anvil snapshots by default (#24666) 1a2903c - - +IPFS hash of the deployment: +- CIDv0: `QmUF5sYAgjigJ6D7nE6p12SG1rFrwYLidKxeX52qBN1Pce` +- CIDv1: `bafybeicxxeljdmh6f2s3xo43piks2h7ibgm6hbfoo3ftc5wjfmqfsngghm` + +The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org). + +You can also access the Uniswap Interface from an IPFS gateway. +**BEWARE**: The Uniswap interface uses [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to remember your settings, such as which tokens you have imported. +**You should always use an IPFS gateway that enforces origin separation**, or our hosted deployment of the latest release at [app.uniswap.org](https://app.uniswap.org). +Your Uniswap settings are never remembered across different URLs. + +IPFS gateways: +- https://bafybeicxxeljdmh6f2s3xo43piks2h7ibgm6hbfoo3ftc5wjfmqfsngghm.ipfs.dweb.link/ +- [ipfs://QmUF5sYAgjigJ6D7nE6p12SG1rFrwYLidKxeX52qBN1Pce/](ipfs://QmUF5sYAgjigJ6D7nE6p12SG1rFrwYLidKxeX52qBN1Pce/) + +## 5.116.0 (2025-10-28) + + +### Features + +* **web:** add activity table to the tab with real data (#23506) f00228c +* **web:** Add createRejectableMockConnector util to force tx rejection (#24574) 3b3b2b7 +* **web:** add demo account support for activity tab (#24639) 9ec0194 +* **web:** add disconnected portfolio view (#23690) 7a1b085 +* **web:** add fiat to price chart (#23577) fab99ce +* **web:** add hidden tokens table rows (#23535) 291fab3 +* **web:** add loading state to tokens table (#23544) ed5ced8 +* **web:** add more & better filtering + transaction parsing (#24579) 205c03d +* **web:** add v2 bridged asset banner (#24734) 4666868 +* **web:** disconnected view B version (#24630) 46ca828 +* **web:** Help Modal styling nits (#24547) ae252e6 +* **web:** NFTs tab (#23604) a438b54 +* **web:** small style nits for Company menu (#24318) 4d71e08 +* **web:** special case metamask dual vm connection flow (#24756) faabc72 +* **web:** tokens table search (#23509) b83fc75 +* **web:** update CompanyMenu arrangement on tablet width (#24312) 758f68d + + +### Bug Fixes + +* **web:** default to mainnet for limits flow [STAGING] (#24885) 5a8e150 +* **web:** Fix CreatePosition e2e anvil test (#24573) d68b011 +* **web:** Fix e2e anvil tests missing quote stub (#24590) 838d5bd +* **web:** Fix limit order chain switch bug (#23064) b11176d +* **web:** Fix Swap e2e anvil tests (#24662) 26adf5c +* **web:** fixes pools tab loader skeletons (#24472) 2f887aa +* **web:** Increase anvil manager timeout (#24623) 466eb69 +* **web:** log interface swap finalization results for flashblocks (#24869) bf30270 +* **web:** support chain filtering query params (#24754) 4bc3729 +* **web:** update the create flow to display the latest dependnet amount (#24676) 168c20a +* **web:** Use Mainnet instead of Base for e2e test commands (#24589) ff7dfee + + +### Continuous Integration + +* **web:** update sitemaps 4e8124b + + +### Tests + +* **web:** Disable anvil snapshots by default (#24666) 1a2903c + + diff --git a/apps/api-self-serve/.eslintrc.js b/apps/api-self-serve/.eslintrc.js new file mode 100644 index 00000000000..bc045be8575 --- /dev/null +++ b/apps/api-self-serve/.eslintrc.js @@ -0,0 +1,44 @@ +const restrictedGlobals = require('confusing-browser-globals') +const rulesDirPlugin = require('eslint-plugin-rulesdir') +rulesDirPlugin.RULES_DIR = '../../packages/uniswap/eslint_rules' + +module.exports = { + root: true, + extends: ['@uniswap/eslint-config/extension'], + plugins: ['rulesdir'], + ignorePatterns: [ + 'node_modules', + '.react-router', + 'dist', + 'build', + '.eslintrc.js', + 'manifest.json', + '.nx', + 'vite.config.ts', + ], + parserOptions: { + project: 'tsconfig.eslint.json', + tsconfigRootDir: __dirname, + ecmaFeatures: { + jsx: true, + }, + ecmaVersion: 2018, + sourceType: 'module', + }, + rules: { + 'rulesdir/i18n': 'error', + }, + overrides: [ + { + files: ['*.ts', '*.tsx'], + rules: { + 'no-relative-import-paths/no-relative-import-paths': [ + 'error', + { + allowSameFolder: false, + }, + ], + }, + }, + ], +} diff --git a/apps/api-self-serve/.gitignore b/apps/api-self-serve/.gitignore new file mode 100644 index 00000000000..039ee62d21a --- /dev/null +++ b/apps/api-self-serve/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +.env +/node_modules/ + +# React Router +/.react-router/ +/build/ diff --git a/apps/api-self-serve/README.md b/apps/api-self-serve/README.md new file mode 100644 index 00000000000..25d1b31c061 --- /dev/null +++ b/apps/api-self-serve/README.md @@ -0,0 +1 @@ +# API Self Serve Portal diff --git a/apps/api-self-serve/app/app.css b/apps/api-self-serve/app/app.css new file mode 100644 index 00000000000..abd90f244db --- /dev/null +++ b/apps/api-self-serve/app/app.css @@ -0,0 +1,170 @@ +@import "tailwindcss/preflight"; +@import "tailwindcss"; +@plugin "tailwindcss-animate"; +@tailwind utilities; +@config "../tailwind.config.ts"; + +@custom-variant dark (&:is(.dark *)); + +@font-face { + font-family: "Basel Grotesk"; + src: url("https://app.uniswap.org/fonts/Basel-Grotesk-Book.woff2") + format("woff2"); + font-weight: 485; + font-style: normal; + font-display: swap; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} + +@font-face { + font-family: "Basel Grotesk"; + src: url("https://app.uniswap.org/fonts/Basel-Grotesk-Medium.woff2") + format("woff2"); + font-weight: 535; + font-style: normal; + font-display: swap; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} + +@layer base { + html, body { + @apply bg-background text-foreground font-basel; + } + * { + @apply border-border outline-ring/50; + } +} + +/* Light mode is the default */ +:root { + color-scheme: light; + --font-basel: "Basel Grotesk"; + /* Light mode shadows */ + --shadow-short: + 0px 1px 6px 2px rgba(0, 0, 0, 0.03), 0px 1px 2px 0px rgba(0, 0, 0, 0.02); + --shadow-medium: + 0px 6px 12px -3px rgba(19, 19, 19, 0.04), + 0px 2px 5px -2px rgba(19, 19, 19, 0.03); + --shadow-large: + 0px 10px 20px -5px rgba(19, 19, 19, 0.05), + 0px 4px 12px -3px rgba(19, 19, 19, 0.04); + /*shadcn*/ + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +/* Dark mode applies when .dark class is present */ +.dark { + color-scheme: dark; + /* Dark mode shadows */ + --shadow-short: + 0px 1px 3px 0px rgba(0, 0, 0, 0.12), 0px 1px 2px 0px rgba(0, 0, 0, 0.24); + --shadow-medium: + 0px 10px 15px -3px rgba(19, 19, 19, 0.54), + 0px 4px 6px -2px rgba(19, 19, 19, 0.4); + --shadow-large: + 0px 16px 24px -6px rgba(0, 0, 0, 0.6), 0px 8px 12px -4px rgba(0, 0, 0, 0.48); + /*shadcn*/ + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +/*shadcn*/ +@theme inline { + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} diff --git a/apps/api-self-serve/app/lib/utils.ts b/apps/api-self-serve/app/lib/utils.ts new file mode 100644 index 00000000000..d32b0fe652e --- /dev/null +++ b/apps/api-self-serve/app/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from 'clsx' +import { twMerge } from 'tailwind-merge' + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/apps/api-self-serve/app/root.tsx b/apps/api-self-serve/app/root.tsx new file mode 100644 index 00000000000..9b3208dc36b --- /dev/null +++ b/apps/api-self-serve/app/root.tsx @@ -0,0 +1,53 @@ +import { isRouteErrorResponse, Links, Meta, Outlet, Scripts, ScrollRestoration } from 'react-router' +import type { Route } from './+types/root' +import './app.css' + +export const links: Route.LinksFunction = () => [] + +export function Layout({ children }: { children: React.ReactNode }) { + return ( + + + + + + + + + {children} + + + + + ) +} + +export default function App() { + return +} + +export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { + let message = 'Oops!' + let details = 'An unexpected error occurred.' + let stack: string | undefined + + if (isRouteErrorResponse(error)) { + message = error.status === 404 ? '404' : 'Error' + details = error.status === 404 ? 'The requested page could not be found.' : error.statusText || details + } else if (import.meta.env.DEV && error && error instanceof Error) { + details = error.message + stack = error.stack + } + + return ( +

+

{message}

+

{details}

+ {stack && ( +
+          {stack}
+        
+ )} +
+ ) +} diff --git a/apps/api-self-serve/app/routes.ts b/apps/api-self-serve/app/routes.ts new file mode 100644 index 00000000000..10d7044bf99 --- /dev/null +++ b/apps/api-self-serve/app/routes.ts @@ -0,0 +1,3 @@ +import { index, type RouteConfig } from '@react-router/dev/routes' + +export default [index('routes/home.tsx')] satisfies RouteConfig diff --git a/apps/api-self-serve/app/routes/home.tsx b/apps/api-self-serve/app/routes/home.tsx new file mode 100644 index 00000000000..c6316aae628 --- /dev/null +++ b/apps/api-self-serve/app/routes/home.tsx @@ -0,0 +1,11 @@ +import { Welcome } from '~/welcome/welcome' +import type { Route } from './+types/home' + +// biome-ignore lint/correctness/noEmptyPattern: this will likely be updated. this is ootb from the create react router app tool. +export function meta({}: Route.MetaArgs) { + return [{ title: 'New React Router App' }, { name: 'description', content: 'Welcome to React Router!' }] +} + +export default function Home() { + return +} diff --git a/apps/api-self-serve/app/welcome/logo-dark.svg b/apps/api-self-serve/app/welcome/logo-dark.svg new file mode 100644 index 00000000000..dd820289447 --- /dev/null +++ b/apps/api-self-serve/app/welcome/logo-dark.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/api-self-serve/app/welcome/logo-light.svg b/apps/api-self-serve/app/welcome/logo-light.svg new file mode 100644 index 00000000000..73284929d36 --- /dev/null +++ b/apps/api-self-serve/app/welcome/logo-light.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/api-self-serve/app/welcome/welcome.tsx b/apps/api-self-serve/app/welcome/welcome.tsx new file mode 100644 index 00000000000..92555f1574a --- /dev/null +++ b/apps/api-self-serve/app/welcome/welcome.tsx @@ -0,0 +1,80 @@ +/** biome-ignore-all lint/correctness/noRestrictedElements: this will be removed, it's default template */ +import logoDark from './logo-dark.svg' +import logoLight from './logo-light.svg' + +export function Welcome() { + return ( +
+ +
+ ) +} + +const resources = [ + { + href: 'https://reactrouter.com/docs', + text: 'React Router Docs', + icon: ( + + + + ), + }, + { + href: 'https://rmx.as/discord', + text: 'Join Discord', + icon: ( + + + + ), + }, +] diff --git a/apps/api-self-serve/components.json b/apps/api-self-serve/components.json new file mode 100644 index 00000000000..d0a566ccc19 --- /dev/null +++ b/apps/api-self-serve/components.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "app/app.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "~/components", + "utils": "~/lib/utils", + "ui": "~/components/ui", + "lib": "~/lib", + "hooks": "~/hooks" + }, + "registries": {} +} diff --git a/apps/api-self-serve/package.json b/apps/api-self-serve/package.json new file mode 100644 index 00000000000..17c95e0eca3 --- /dev/null +++ b/apps/api-self-serve/package.json @@ -0,0 +1,36 @@ +{ + "name": "api-self-serve", + "private": true, + "type": "module", + "scripts": { + "build": "react-router build", + "dev": "react-router dev", + "start": "react-router-serve ./build/server/index.js", + "typecheck": "react-router typegen && tsc" + }, + "dependencies": { + "@react-router/node": "7.6.3", + "@react-router/serve": "7.6.3", + "class-variance-authority": "0.7.1", + "clsx": "2.1.1", + "isbot": "5.1.31", + "lucide-react": "0.548.0", + "react": "19.0.0", + "react-dom": "19.0.0", + "react-router": "7.6.3", + "tailwind-merge": "3.3.1", + "tailwindcss-animate": "1.0.7" + }, + "devDependencies": { + "@react-router/dev": "7.6.3", + "@tailwindcss/vite": "4.1.13", + "@types/node": "22.13.1", + "@types/react": "19.0.10", + "@uniswap/eslint-config": "workspace:^", + "eslint": "8.57.1", + "tailwindcss": "4.1.16", + "typescript": "5.8.3", + "vite": "npm:rolldown-vite@7.0.10", + "vite-tsconfig-paths": "5.1.4" + } +} diff --git a/apps/api-self-serve/react-router.config.ts b/apps/api-self-serve/react-router.config.ts new file mode 100644 index 00000000000..6ff16f91779 --- /dev/null +++ b/apps/api-self-serve/react-router.config.ts @@ -0,0 +1,7 @@ +import type { Config } from "@react-router/dev/config"; + +export default { + // Config options... + // Server-side render by default, to enable SPA mode set this to `false` + ssr: true, +} satisfies Config; diff --git a/apps/api-self-serve/tailwind.config.ts b/apps/api-self-serve/tailwind.config.ts new file mode 100644 index 00000000000..8135de497d5 --- /dev/null +++ b/apps/api-self-serve/tailwind.config.ts @@ -0,0 +1,430 @@ +import type { Config } from 'tailwindcss' +import tailwindAnimate from 'tailwindcss-animate' + +export default { + darkMode: 'class', + content: [ + './pages/**/*.{js,ts,jsx,tsx,mdx}', + './components/**/*.{js,ts,jsx,tsx,mdx}', + './app/**/*.{js,ts,jsx,tsx,mdx}', + './registry/**/*.{js,ts,jsx,tsx,mdx}', + ], + theme: { + extend: { + fontFamily: { + basel: ['var(--font-basel)', 'sans-serif'], + baselBook: [ + 'Basel Grotesk Book', + '-apple-system', + 'system-ui', + 'BlinkMacSystemFont', + 'Segoe UI', + 'Roboto', + 'Helvetica', + 'Arial', + 'sans-serif', + ], + baselMedium: [ + 'Basel Grotesk Medium', + '-apple-system', + 'system-ui', + 'BlinkMacSystemFont', + 'Segoe UI', + 'Roboto', + 'Helvetica', + 'Arial', + 'sans-serif', + ], + mono: ['InputMono-Regular', 'monospace'], + }, + fontWeight: { + book: '485', + medium: '535', + }, + screens: { + xxs: '360px', + xs: '380px', + sm: '450px', + md: '640px', + lg: '768px', + xl: '1024px', + xxl: '1280px', + xxxl: '1536px', + 'h-short': { raw: '(max-height: 736px)' }, + 'h-mid': { raw: '(max-height: 800px)' }, + }, + fontSize: { + // Headings + 'heading-1': [ + '52px', + { + lineHeight: '60px', + letterSpacing: '-0.02em', + fontWeight: '485', + }, + ], + 'heading-2': [ + '36px', + { + lineHeight: '44px', + letterSpacing: '-0.01em', + fontWeight: '485', + }, + ], + 'heading-3': [ + '24px', + { + lineHeight: '32px', + letterSpacing: '-0.005em', + fontWeight: '485', + }, + ], + // Subheadings + 'subheading-1': [ + '18px', + { + lineHeight: '24px', + fontWeight: '485', + }, + ], + 'subheading-2': [ + '16px', + { + lineHeight: '24px', + fontWeight: '485', + }, + ], + // Body + 'body-1': [ + '18px', + { + lineHeight: '24px', + fontWeight: '485', + }, + ], + 'body-2': [ + '16px', + { + lineHeight: '24px', + fontWeight: '485', + }, + ], + 'body-3': [ + '14px', + { + lineHeight: '20px', + fontWeight: '485', + }, + ], + 'body-4': [ + '12px', + { + lineHeight: '16px', + fontWeight: '485', + }, + ], + // Button Labels + 'button-1': [ + '18px', + { + lineHeight: '24px', + fontWeight: '535', + }, + ], + 'button-2': [ + '16px', + { + lineHeight: '24px', + fontWeight: '535', + }, + ], + 'button-3': [ + '14px', + { + lineHeight: '20px', + fontWeight: '535', + }, + ], + 'button-4': [ + '12px', + { + lineHeight: '16px', + fontWeight: '535', + }, + ], + }, + colors: { + // Base colors + white: '#FFFFFF', + black: '#000000', + + // Semantic colors for light theme + background: { + DEFAULT: '#FFFFFF', // colors.white + dark: '#000000', // colors.black + }, + + // Neutral colors with semantic naming + neutral1: { + DEFAULT: '#222222', // neutral1_light + dark: '#FFFFFF', // neutral1_dark + }, + neutral2: { + DEFAULT: '#7D7D7D', // neutral2_light + dark: '#9B9B9B', // neutral2_dark + }, + neutral3: { + DEFAULT: '#CECECE', // neutral3_light + dark: '#5E5E5E', // neutral3_dark + }, + + // Surface colors with semantic naming + surface1: { + DEFAULT: '#FFFFFF', // surface1_light + dark: '#131313', // surface1_dark + hovered: { + DEFAULT: '#F5F5F5', // surface1_hovered_light + dark: '#181818', // surface1_hovered_dark + }, + }, + surface2: { + DEFAULT: '#F9F9F9', // surface2_light + dark: '#1B1B1B', // surface2_dark + hovered: { + DEFAULT: '#F2F2F2', // surface2_hovered_light + dark: '#242424', // surface2_hovered_dark + }, + }, + surface3: { + DEFAULT: '#22222212', // surface3_light + dark: '#FFFFFF12', // surface3_dark + hovered: { + DEFAULT: 'rgba(34, 34, 34, 0.12)', // surface3_hovered_light + dark: 'rgba(255, 255, 255, 0.16)', // surface3_hovered_dark + }, + }, + surface4: { + DEFAULT: '#FFFFFF64', // surface4_light + dark: '#FFFFFF20', // surface4_dark + }, + surface5: { + DEFAULT: '#00000004', // surface5_light + dark: '#00000004', // surface5_dark + }, + + // Accent colors with semantic naming + accent1: { + DEFAULT: '#FC72FF', // accent1_light + dark: '#FC72FF', // accent1_dark + }, + accent2: { + DEFAULT: '#FFEFFF', // accent2_light + dark: '#311C31', // accent2_dark + }, + accent3: { + DEFAULT: '#4C82FB', // accent3_light + dark: '#4C82FB', // accent3_dark + }, + + // Token colors + token0: { + DEFAULT: '#FC72FF', // token0 in light theme + dark: '#FC72FF', // token0 in dark theme + }, + token1: { + DEFAULT: '#4C82FB', // token1 in light theme + dark: '#4C82FB', // token1 in dark theme + }, + + // Status colors + success: { + DEFAULT: '#40B66B', // success + }, + critical: { + DEFAULT: '#FF5F52', // critical + secondary: { + DEFAULT: '#FFF2F1', // critical2_light + dark: '#2E0805', // critical2_dark + }, + }, + warning: { + DEFAULT: '#EEB317', // gold200 + }, + + // Network colors + network: { + ethereum: '#627EEA', + optimism: '#FF0420', + polygon: '#A457FF', + arbitrum: '#28A0F0', + bsc: '#F0B90B', + base: '#0052FF', + blast: '#FCFC03', + }, + + // Gray palette + gray: { + 50: '#F5F6FC', + 100: '#E8ECFB', + 150: '#D2D9EE', + 200: '#B8C0DC', + 250: '#A6AFCA', + 300: '#98A1C0', + 350: '#888FAB', + 400: '#7780A0', + 450: '#6B7594', + 500: '#5D6785', + 550: '#505A78', + 600: '#404A67', + 650: '#333D59', + 700: '#293249', + 750: '#1B2236', + 800: '#131A2A', + 850: '#0E1524', + 900: '#0D111C', + 950: '#080B11', + }, + + // Pink palette + pink: { + 50: '#F9ECF1', + 100: '#FFD9E4', + 200: '#FBA4C0', + 300: '#FF6FA3', + 400: '#FB118E', + 500: '#C41969', + 600: '#8C0F49', + 700: '#55072A', + 800: '#350318', + 900: '#2B000B', + vibrant: '#F50DB4', + base: '#FC74FE', + }, + + // Red palette + red: { + 50: '#FAECEA', + 100: '#FED5CF', + 200: '#FEA79B', + 300: '#FD766B', + 400: '#FA2B39', + 500: '#C4292F', + 600: '#891E20', + 700: '#530F0F', + 800: '#380A03', + 900: '#240800', + vibrant: '#F14544', + }, + + // Additional color palettes + yellow: { + 50: '#F6F2D5', + 100: '#DBBC19', + 200: '#DBBC19', + 300: '#BB9F13', + 400: '#A08116', + 500: '#866311', + 600: '#5D4204', + 700: '#3E2B04', + 800: '#231902', + 900: '#180F02', + vibrant: '#FAF40A', + }, + + green: { + 50: '#E3F3E6', + 100: '#BFEECA', + 200: '#76D191', + 300: '#40B66B', + 400: '#209853', + 500: '#0B783E', + 600: '#0C522A', + 700: '#053117', + 800: '#091F10', + 900: '#09130B', + vibrant: '#5CFE9D', + }, + + blue: { + 50: '#EDEFF8', + 100: '#DEE1FF', + 200: '#ADBCFF', + 300: '#869EFF', + 400: '#4C82FB', + 500: '#1267D6', + 600: '#1D4294', + 700: '#09265E', + 800: '#0B193F', + 900: '#040E34', + vibrant: '#587BFF', + }, + + gold: { + 200: '#EEB317', + 400: '#B17900', + vibrant: '#FEB239', + }, + + magenta: { + 300: '#FD82FF', + vibrant: '#FC72FF', + }, + + purple: { + 300: '#8440F2', + 900: '#1C0337', + vibrant: '#6100FF', + }, + + // Legacy colors mapping (for compatibility) + border: '#F9F9F9', + input: '#F9F9F9', + ring: '#222222', + foreground: '#222222', + card: { + DEFAULT: '#FFFFFF', + foreground: '#222222', + }, + popover: { + DEFAULT: '#FFFFFF', + foreground: '#222222', + }, + primary: { + DEFAULT: '#222222', + foreground: '#F9F9F9', + }, + secondary: { + DEFAULT: '#F9F9F9', + foreground: '#222222', + }, + muted: { + DEFAULT: '#F9F9F9', + foreground: '#7D7D7D', + }, + destructive: { + DEFAULT: '#FF5F52', + foreground: '#F9F9F9', + }, + scrim: 'rgba(0, 0, 0, 0.60)', + }, + borderRadius: { + none: '0px', + rounded4: '4px', + rounded6: '6px', + rounded8: '8px', + rounded12: '12px', + rounded16: '16px', + rounded20: '20px', + rounded24: '24px', + rounded32: '32px', + roundedFull: '999999px', + }, + boxShadow: { + short: 'var(--shadow-short)', + medium: 'var(--shadow-medium)', + large: 'var(--shadow-large)', + }, + }, + }, + plugins: [tailwindAnimate], +} satisfies Config diff --git a/apps/api-self-serve/tsconfig.eslint.json b/apps/api-self-serve/tsconfig.eslint.json new file mode 100644 index 00000000000..0af7bb26f3f --- /dev/null +++ b/apps/api-self-serve/tsconfig.eslint.json @@ -0,0 +1,5 @@ +// same as tsconfig.json but without references which caused performance issues with typescript-eslint +{ + "extends": "./tsconfig.json", + "references": [] +} diff --git a/apps/api-self-serve/tsconfig.json b/apps/api-self-serve/tsconfig.json new file mode 100644 index 00000000000..a0d80e99890 --- /dev/null +++ b/apps/api-self-serve/tsconfig.json @@ -0,0 +1,23 @@ +{ + "include": ["**/*", "**/.server/**/*", "**/.client/**/*", ".react-router/types/**/*"], + "exclude": ["tools/**/*"], + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "types": ["node", "vite/client"], + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "rootDirs": [".", "./.react-router/types"], + "baseUrl": ".", + "paths": { + "~/*": ["./app/*"] + }, + "esModuleInterop": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true + } +} diff --git a/apps/api-self-serve/vite.config.ts b/apps/api-self-serve/vite.config.ts new file mode 100644 index 00000000000..e0925ec7ad1 --- /dev/null +++ b/apps/api-self-serve/vite.config.ts @@ -0,0 +1,15 @@ +import { reactRouter } from '@react-router/dev/vite' +import tailwindcss from '@tailwindcss/vite' +import { defineConfig } from 'vite' +import tsconfigPaths from 'vite-tsconfig-paths' + +export default defineConfig({ + plugins: [ + tailwindcss(), + reactRouter(), + tsconfigPaths({ + // ignores tsconfig files in Nx generator template directories + skip: (dir) => dir.includes('files'), + }), + ], +}) diff --git a/apps/cli/.eslintrc.cjs b/apps/cli/.eslintrc.cjs new file mode 100644 index 00000000000..e2b34a6c0b6 --- /dev/null +++ b/apps/cli/.eslintrc.cjs @@ -0,0 +1,45 @@ +module.exports = { + root: true, + extends: ['@uniswap/eslint-config/native', '@uniswap/eslint-config/webPlatform'], + ignorePatterns: [ + 'node_modules', + '.turbo', + '.eslintrc.js', + 'vitest.config.ts', + 'codegen.ts', + '.nx', + 'scripts', + 'dist', + 'src/**/__generated__', + ], + parserOptions: { + project: 'tsconfig.lint.json', + tsconfigRootDir: __dirname, + ecmaFeatures: { + jsx: true, + }, + ecmaVersion: 2018, + sourceType: 'module', + }, + overrides: [ + { + files: ['src/index.ts'], + rules: { + 'check-file/no-index': 'off', + }, + }, + { + files: ['*.ts', '*.tsx'], + rules: { + 'no-relative-import-paths/no-relative-import-paths': [ + 'error', + { + allowSameFolder: false, + prefix: '@universe/cli', + }, + ], + }, + }, + ], + rules: {}, +} diff --git a/apps/cli/README.md b/apps/cli/README.md new file mode 100644 index 00000000000..ce9112633d7 --- /dev/null +++ b/apps/cli/README.md @@ -0,0 +1,2 @@ +# @universe/cli + diff --git a/apps/cli/package.json b/apps/cli/package.json new file mode 100644 index 00000000000..86b22c11c5c --- /dev/null +++ b/apps/cli/package.json @@ -0,0 +1,38 @@ +{ + "name": "@universe/cli", + "version": "0.0.0", + "type": "module", + "dependencies": { + "@ai-sdk/anthropic": "2.0.41", + "ai": "5.0.87", + "ink": "5.2.1", + "ink-box": "2.0.0", + "ink-select-input": "6.2.0", + "ink-spinner": "5.0.0", + "ink-text-input": "6.0.0", + "react": "19.0.0" + }, + "devDependencies": { + "@types/bun": "1.3.1", + "@types/node": "22.13.1", + "@types/react": "19.0.10", + "@uniswap/eslint-config": "workspace:^", + "depcheck": "1.4.7", + "eslint": "8.57.1", + "ink-gradient": "3.0.0", + "typescript": "5.8.3" + }, + "scripts": { + "dev": "bun run --watch src/cli-ui.tsx", + "lint:biome": "nx lint:biome cli", + "lint:biome:fix": "nx lint:biome:fix cli", + "lint": "nx lint cli", + "lint:fix": "nx lint:fix cli" + }, + "nx": { + "includedScripts": [] + }, + "main": "src/index.ts", + "private": true, + "sideEffects": false +} diff --git a/apps/cli/project.json b/apps/cli/project.json new file mode 100644 index 00000000000..a9dc79ffbd2 --- /dev/null +++ b/apps/cli/project.json @@ -0,0 +1,19 @@ +{ + "name": "@universe/cli", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "apps/cli/src", + "projectType": "application", + "tags": [], + "targets": { + "build": { + "executor": "nx:noop" + }, + "typecheck": {}, + "lint:biome": {}, + "lint:biome:fix": {}, + "lint:eslint": {}, + "lint:eslint:fix": {}, + "lint": {}, + "lint:fix": {} + } +} diff --git a/apps/cli/src/cli-ui.tsx b/apps/cli/src/cli-ui.tsx new file mode 100644 index 00000000000..731955fdc03 --- /dev/null +++ b/apps/cli/src/cli-ui.tsx @@ -0,0 +1,27 @@ +#!/usr/bin/env bun +import { App } from '@universe/cli/src/ui/App' +import { render } from 'ink' +// Ensure React is loaded before ink +import React from 'react' + +export async function runUI(): Promise { + // Check for API key + if (!process.env.ANTHROPIC_API_KEY) { + // eslint-disable-next-line no-console + console.error('Error: ANTHROPIC_API_KEY environment variable is required') + // eslint-disable-next-line no-console + console.error('Get your API key from: https://console.anthropic.com/') + process.exit(1) + } + + render(React.createElement(App)) +} + +// Run if executed directly +if (import.meta.main) { + runUI().catch((error) => { + // eslint-disable-next-line no-console + console.error('Unhandled error:', error) + process.exit(1) + }) +} diff --git a/apps/cli/src/cli.ts b/apps/cli/src/cli.ts new file mode 100644 index 00000000000..3ac8f3939b9 --- /dev/null +++ b/apps/cli/src/cli.ts @@ -0,0 +1,429 @@ +#!/usr/bin/env bun +/* eslint-disable complexity */ +import { type CollectOptions } from '@universe/cli/src/core/data-collector' +import { Orchestrator, type OrchestratorConfig, type OutputConfig } from '@universe/cli/src/core/orchestrator' +import { createVercelAIProvider } from '@universe/cli/src/lib/ai-provider-vercel' +import { SqliteCacheProvider } from '@universe/cli/src/lib/cache-provider-sqlite' +import { ConsoleLogger, type Logger } from '@universe/cli/src/lib/logger' +import { parseReleaseIdentifier, ReleaseScanner } from '@universe/cli/src/lib/release-scanner' +import { detectRepository, resolveTeam } from '@universe/cli/src/lib/team-resolver' +import { parseArgs } from 'util' + +/* eslint-disable no-console */ + +// ============================================================================ +// CLI Configuration +// ============================================================================ + +async function main(): Promise { + const { values, positionals } = parseArgs({ + args: Bun.argv, + options: { + // UI options + interactive: { type: 'boolean', default: false, description: 'Force interactive UI mode' }, + ui: { type: 'boolean', default: false, description: 'Force interactive UI mode (alias for --interactive)' }, + + // Analysis options + mode: { + type: 'string', + description: 'Analysis mode (team-digest, changelog, release-changelog, bug-bisect, etc.)', + }, + prompt: { type: 'string', description: 'Custom prompt (file path or inline text)' }, + bug: { type: 'string', description: 'Bug description for bug-bisect mode (requires --release)' }, + + // Data filtering + team: { type: 'string', description: 'Team filter (@org/team or user1,user2)' }, + since: { type: 'string', default: '30 days ago', description: 'Time period to analyze' }, + repo: { type: 'string', description: 'Repository (owner/name)' }, + 'include-open-prs': { type: 'boolean', default: false, description: 'Include open PRs' }, + + // Release options + release: { type: 'string', description: 'Release to analyze (e.g., mobile/1.60 or extension/1.30.0)' }, + 'compare-with': { type: 'string', description: 'Specific version to compare with' }, + 'list-releases': { type: 'boolean', default: false, description: 'List available releases' }, + platform: { type: 'string', description: 'Platform filter (mobile or extension)' }, + + // Commit data options + 'include-diffs': { type: 'boolean', default: false, description: 'Include actual diff content in commits' }, + 'max-diff-size': { type: 'string', default: '100', description: 'Max lines changed per file to include diff' }, + 'max-diff-files': { type: 'string', default: '20', description: 'Max number of files to include diffs for' }, + 'diff-pattern': { type: 'string', description: 'Regex pattern for files to include diffs' }, + 'exclude-test-diffs': { type: 'boolean', default: true, description: 'Exclude test files from diffs' }, + 'token-budget': { type: 'string', default: '50000', description: 'Approximate token budget for commit data' }, + 'pr-body-limit': { type: 'string', default: '2000', description: 'Max characters for PR body' }, + 'save-artifacts': { type: 'boolean', default: false, description: 'Save analysis artifacts for debugging' }, + + // Output options + output: { type: 'string', multiple: true, description: 'Output targets (can specify multiple)' }, + + // Other options + verbose: { type: 'boolean', default: false, description: 'Verbose logging' }, + 'dry-run': { type: 'boolean', default: false, description: 'Test mode without publishing' }, + 'no-cache': { type: 'boolean', default: false, description: 'Bypass cache and fetch fresh data' }, + 'force-refresh': { + type: 'boolean', + default: false, + description: 'Bypass cache and fetch fresh data (alias for --no-cache)', + }, + help: { type: 'boolean', default: false, description: 'Show help' }, + }, + strict: true, + allowPositionals: true, + }) + + if (values.help) { + showHelp() + process.exit(0) + } + + // Detect if we should use UI mode + const shouldUseUI = + values.interactive || + values.ui || + (!values.release && + !values.mode && + !values.prompt && + !values.team && + !values['list-releases'] && + !values.output && + positionals.length === 0) + + if (shouldUseUI) { + throw new Error('UI mode is not supported') + } + + try { + // Create logger early for consistent logging + const logger = new ConsoleLogger(values.verbose || false) + + // Handle --list-releases + if (values['list-releases']) { + const scanner = new ReleaseScanner(process.cwd(), logger) + const platform = values.platform as 'mobile' | 'extension' | undefined + await scanner.listReleases(platform) + process.exit(0) + } + + // Check for API key + if (!process.env.ANTHROPIC_API_KEY) { + logger.error('Error: ANTHROPIC_API_KEY environment variable is required') + logger.error('Get your API key from: https://console.anthropic.com/') + process.exit(1) + } + + // Build configuration + const config = await buildConfig(values, logger) + + if (values.verbose) { + logger.debug(`Configuration: ${JSON.stringify(config, null, 2)}`) + } + + // Create cache provider (unless bypassing cache) + const bypassCache = values['no-cache'] || values['force-refresh'] + const cacheProvider = bypassCache ? undefined : new SqliteCacheProvider() + + // Run orchestrator + const aiProvider = createVercelAIProvider(process.env.ANTHROPIC_API_KEY) + const orchestrator = new Orchestrator({ + config, + aiProvider, + cacheProvider, + logger, + }) + await orchestrator.execute() + + // Close cache connection if used + if (cacheProvider) { + cacheProvider.close() + } + + logger.info('✨ Analysis complete!') + } catch (error) { + // Create a minimal logger if we don't have one yet + const errorLogger = new ConsoleLogger(false) + errorLogger.error(`Fatal error: ${error}`) + process.exit(1) + } +} + +interface BuildConfigArgs { + repo?: string + team?: string + since?: string + 'include-open-prs'?: boolean + release?: string + 'compare-with'?: string + mode?: string + prompt?: string + bug?: string + 'include-diffs'?: boolean + 'max-diff-size'?: string + 'max-diff-files'?: string + 'diff-pattern'?: string + 'exclude-test-diffs'?: boolean + 'token-budget'?: string + 'pr-body-limit'?: string + output?: string[] + verbose?: boolean + 'dry-run'?: boolean + 'save-artifacts'?: boolean + 'no-cache'?: boolean + 'force-refresh'?: boolean +} + +async function buildConfig(args: BuildConfigArgs, logger: Logger): Promise { + // Parse repository + let repository: { owner?: string; name?: string } | undefined + + if (args.repo) { + const match = args.repo.match(/^([^/]+)\/([^/]+)$/) + if (!match) { + throw new Error(`Invalid repository format: "${args.repo}". Expected: owner/repo`) + } + repository = { owner: match[1], name: match[2] } + } else { + // Try to detect from git + repository = (await detectRepository()) || undefined + } + + if (repository) { + logger.info(`Repository: ${repository.owner}/${repository.name}`) + } + + // Resolve team if specified + let teamFilter: string[] | undefined + let teamUsernames: string[] | undefined + + if (args.team) { + logger.info(`Resolving team: ${args.team}`) + const resolution = await resolveTeam(args.team) + teamFilter = resolution.emails + teamUsernames = resolution.usernames + + if (teamFilter.length === 0) { + throw new Error('Failed to resolve team filter') + } + logger.info(`Team resolved to ${teamFilter.length} email(s)`) + if (args.verbose) { + logger.debug(`Emails: ${teamFilter}`) + logger.debug(`Usernames: ${teamUsernames}`) + } + } + + // Parse outputs + const outputs = parseOutputs(args.output || ['stdout']) + + // Handle release mode + let releaseOptions + if (args.release) { + const releaseId = parseReleaseIdentifier(args.release) + if (!releaseId) { + throw new Error(`Invalid release format: "${args.release}". Expected: mobile/1.60 or extension/1.30.0`) + } + + let version = releaseId.version + + // Handle "latest" keyword + if (version === 'latest') { + const scanner = new ReleaseScanner(process.cwd(), logger) + const latestRelease = await scanner.getLatestRelease(releaseId.platform) + + if (!latestRelease) { + throw new Error(`No releases found for platform: ${releaseId.platform}`) + } + + version = latestRelease.version + logger.info(`Using latest ${releaseId.platform} release: ${version}`) + } + + releaseOptions = { + platform: releaseId.platform, + version, + compareWith: args['compare-with'], + } + + // Auto-set mode to release-changelog if not explicitly set + if (!args.mode) { + args.mode = 'release-changelog' + } + } + + // Build commit data config + const commitDataConfig = { + includeFilePaths: true, // Always include file paths + includeDiffs: args['include-diffs'], + maxDiffSize: parseInt(args['max-diff-size'] || '100', 10), + maxDiffFiles: parseInt(args['max-diff-files'] || '20', 10), + diffFilePattern: args['diff-pattern'], + excludeTestFiles: args['exclude-test-diffs'] !== false, + tokenBudget: parseInt(args['token-budget'] || '50000', 10), + prBodyLimit: parseInt(args['pr-body-limit'] || '2000', 10), + } + + // Build collection options + const collectOptions: CollectOptions = { + since: args.since ?? '30 days ago', + repository, + teamFilter, + teamUsernames, + includeOpenPrs: args['include-open-prs'], + commitDataConfig, + } + + // Handle bug-bisect mode + if (args.bug) { + if (!args.release) { + throw new Error('--bug requires --release to be specified') + } + // Auto-set mode to bug-bisect if not explicitly set + if (!args.mode) { + args.mode = 'bug-bisect' + } else if (args.mode !== 'bug-bisect') { + throw new Error('--bug can only be used with --mode bug-bisect') + } + } + + // Build analysis config + const analysisConfig = { + mode: args.mode, + prompt: args.prompt, + releaseOptions, + variables: args.bug + ? { + BUG_DESCRIPTION: args.bug, + } + : undefined, + } + + return { + analysis: analysisConfig, + outputs, + collect: collectOptions, + verbose: args.verbose, + dryRun: args['dry-run'], + saveArtifacts: args['save-artifacts'], + bypassCache: args['no-cache'] || args['force-refresh'] || false, + } +} + +function parseOutputs(outputs: string[]): OutputConfig[] { + return outputs.map((output) => { + // Parse format: type:target + // Examples: + // stdout + // file:report.md + // slack:#channel + // github-release + + const parts = output.split(':') + const type = parts[0] + const target = parts.slice(1).join(':') // Handle colons in target + + if (!type) { + throw new Error(`Invalid output format: "${output}"`) + } + + return { + type, + target: target || undefined, + } + }) +} + +function showHelp(): void { + console.log(` +Repository Intelligence System - Analyze git history with AI + +Usage: bun scripts/gh-agent-refactored.ts [options] + +ANALYSIS OPTIONS: + --mode Predefined analysis mode (team-digest, changelog, release-changelog, bug-bisect) + --prompt Custom prompt (file path or inline text) + Examples: + --prompt ./my-analysis.md + --prompt "Analyze for security issues" + --bug Bug description for bug-bisect mode (requires --release) + Example: + --bug "Users can't connect wallet on mobile app" + --release mobile/1.60 --bug "Crash on launch" + +DATA FILTERING: + --team Team filter (@org/team or user1,user2) + Examples: + --team @Uniswap/apps-swap + --team alice,bob + --team @Uniswap/backend,external-contributor + --since Time period to analyze (default: "30 days ago") + --repo Repository to analyze (auto-detected if not specified) + --include-open-prs Include open/in-review PRs in analysis + +RELEASE OPTIONS: + --release Release to analyze (e.g., mobile/1.60, extension/1.30.0, or mobile/latest) + --compare-with Specific version to compare with (auto-detects if not specified) + --list-releases List available releases + --platform Platform filter for --list-releases (mobile or extension) + +OUTPUT OPTIONS: + --output Output target (can specify multiple) + Examples: + --output stdout (default) + --output file:report.md + --output slack:#channel + Multiple outputs: + --output file:report.md --output slack:#updates + +OTHER OPTIONS: + --verbose Enable verbose logging + --dry-run Test mode without publishing + --no-cache Bypass cache and fetch fresh data + --force-refresh Bypass cache and fetch fresh data (alias for --no-cache) + --help Show this help message + +EXAMPLES: + # Team digest with default settings + bun scripts/gh-agent-refactored.ts --mode team-digest --team @Uniswap/apps-swap + + # Weekly changelog + bun scripts/gh-agent-refactored.ts --mode changelog --since "1 week ago" + + # Release changelog for mobile + bun scripts/gh-agent-refactored.ts --release mobile/1.60 + + # Release changelog for latest mobile release + bun scripts/gh-agent-refactored.ts --release mobile/latest + + # Release changelog with specific comparison + bun scripts/gh-agent-refactored.ts --release mobile/1.60 --compare-with mobile/1.58 + + # Bug bisect - find which commit introduced a bug + bun scripts/gh-agent-refactored.ts --release mobile/1.60 --bug "Users can't connect wallet" + + # List all releases + bun scripts/gh-agent-refactored.ts --list-releases + + # List mobile releases only + bun scripts/gh-agent-refactored.ts --list-releases --platform mobile + + # Custom analysis with multiple outputs + bun scripts/gh-agent-refactored.ts \\ + --prompt "Analyze for performance improvements" \\ + --team alice,bob \\ + --output file:performance.md \\ + --output slack:#perf-updates + +ENVIRONMENT VARIABLES: + ANTHROPIC_API_KEY Required - Your Anthropic API key + SLACK_WEBHOOK Optional - Webhook URL for Slack integration + +For more information, see the documentation at: +https://github.com/Uniswap/universe/scripts/gh-agent/README.md +`) +} + +// Run if executed directly +if (import.meta.main) { + main().catch((error) => { + console.error('Unhandled error:', error) + process.exit(1) + }) +} diff --git a/apps/cli/src/core/data-collector.ts b/apps/cli/src/core/data-collector.ts new file mode 100644 index 00000000000..fd9979b9cc6 --- /dev/null +++ b/apps/cli/src/core/data-collector.ts @@ -0,0 +1,1096 @@ +/* eslint-disable max-lines */ +/* eslint-disable max-depth */ +/* eslint-disable complexity */ +/* eslint-disable max-params */ + +/** biome-ignore-all lint/suspicious/noConsole: CLI tool requires console output */ + +import { getCommitsCacheKey, getPullRequestsCacheKey, getStatsCacheKey } from '@universe/cli/src/lib/cache-keys' +import { type CacheProvider } from '@universe/cli/src/lib/cache-provider' +import type { Logger } from '@universe/cli/src/lib/logger' +import { cleanPRBody } from '@universe/cli/src/lib/pr-body-cleaner' +import { type ReleaseComparison } from '@universe/cli/src/lib/release-scanner' +import { isTrivialFile } from '@universe/cli/src/lib/trivial-files' +import { $ } from 'bun' + +// ============================================================================ +// Data Collection Types +// ============================================================================ + +export interface CommitDataConfig { + includeFilePaths?: boolean // Include file paths in commit data (default: true) + includeDiffs?: boolean // Include actual diff content (default: false) + maxDiffSize?: number // Max lines changed per file to include diff (default: 100) + maxDiffFiles?: number // Max number of files to include diffs for (default: 20) + diffFilePattern?: string // Regex pattern for files to include diffs (default: \.(ts|tsx|js|jsx)$) + excludeTestFiles?: boolean // Exclude test files from diffs (default: true) + tokenBudget?: number // Approximate token budget for all commit data (default: 50000) + prBodyLimit?: number // Max characters for PR body (default: 2000) + cleanPRBodies?: boolean // Enable PR body cleaning (default: true) +} + +export interface CollectOptions { + since: string + branch?: string + author?: string + repoPath?: string + includeOpenPrs?: boolean + teamFilter?: string[] + teamUsernames?: string[] + repository?: { owner?: string; name?: string } + releaseComparison?: ReleaseComparison + excludeTrivialCommits?: boolean // Filter out commits with only lockfile/snapshot changes + commitDataConfig?: CommitDataConfig // Configuration for commit data collection +} + +export interface RepositoryData { + commits: Commit[] + pullRequests: PullRequest[] + stats: StatsOutput + metadata: { + repository: string + period: string + collectedAt: Date + commitCount: number + prCount: number + releaseInfo?: { + from: string + to: string + platform: 'mobile' | 'extension' + } + filtering?: { + totalCommitsFound: number + trivialCommitsFiltered: number + filesProcessed: number + trivialFilesSkipped: number + } + } +} + +export interface Commit { + sha: string + author: { name: string; email: string } + timestamp: Date + message: string + stats: { filesChanged: number; insertions: number; deletions: number } + files?: { + path: string + status: 'added' | 'modified' | 'deleted' | 'renamed' + additions: number + deletions: number + diff?: string // Optional: actual diff content + diffTruncated?: boolean // Optional: indicates if diff was truncated + }[] +} + +export interface PullRequest { + number: number + title: string + body: string + author: string + state: 'open' | 'closed' + mergedAt: string + mergeCommitSha?: string +} + +export interface StatsOutput { + totalCommits: number + totalAuthors: number + filesChanged: number + linesAdded: number + linesDeleted: number +} + +// ============================================================================ +// Helper Functions for Git Output Parsing +// ============================================================================ + +/** + * Validates and parses a git log line in the format: sha|email|name|timestamp|message + * Returns undefined if the line is malformed + */ +function parseGitLogLine( + line: string, +): { sha: string; email: string; name: string; timestamp: string; message: string } | undefined { + const parts = line.split('|') + if (parts.length < 5) { + return undefined + } + + const sha = parts[0]?.trim() + const email = parts[1]?.trim() + const name = parts[2]?.trim() + const timestamp = parts[3]?.trim() + const messageParts = parts.slice(4) + const message = messageParts.join('|') + + if (!sha || !email || !name || !timestamp) { + return undefined + } + + return { sha, email, name, timestamp, message } +} + +/** + * Validates and parses a git numstat line in the format: additions\tdeletions\tfilepath + * Returns undefined if the line is malformed + */ +function parseNumstatLine(line: string): { additions: string; deletions: string; filepath: string } | undefined { + const parts = line.split('\t') + if (parts.length < 3) { + return undefined + } + + const additions = parts[0]?.trim() + const deletions = parts[1]?.trim() + const filepath = parts.slice(2).join('\t') // Handle filepaths with tabs + + if (additions === undefined || deletions === undefined || !filepath) { + return undefined + } + + return { additions, deletions, filepath } +} + +// ============================================================================ +// Data Collector +// ============================================================================ + +export class DataCollector { + private filteringStats = { + totalCommitsFound: 0, + trivialCommitsFiltered: 0, + filesProcessed: 0, + trivialFilesSkipped: 0, + } + private tokenUsage = 0 + private diffsCollected = 0 + + constructor( + private repoPath: string = process.cwd(), + private cacheProvider?: CacheProvider, + private bypassCache: boolean = false, + private logger?: Logger, + ) {} + + /** + * Estimate tokens for a given text (rough approximation: 1 token ≈ 4 characters) + */ + private estimateTokens(text: string): number { + return Math.ceil(text.length / 4) + } + + /** + * Check if a file should have its diff included based on config + */ + private shouldIncludeDiff(filePath: string, additions: number, deletions: number, config: CommitDataConfig): boolean { + // Check if diffs are enabled + if (!config.includeDiffs) { + return false + } + + // Check if we've hit the diff file limit + if (this.diffsCollected >= (config.maxDiffFiles || 20)) { + return false + } + + // Check if we're within token budget + const budget = config.tokenBudget || 50000 // Can be higher with compact format + if (this.tokenUsage >= budget) { + this.logger?.info(`Token budget reached (${this.tokenUsage}/${budget}), skipping remaining diffs`) + return false + } + + // Check file size limit + const totalChanges = additions + deletions + if (totalChanges > (config.maxDiffSize || 100)) { + return false + } + + // Check if it's a test file and we're excluding them + if (config.excludeTestFiles !== false) { + const testPatterns = [ + /\.test\.(ts|tsx|js|jsx)$/, + /\.spec\.(ts|tsx|js|jsx)$/, + /__tests__\//, + /test\//i, + /tests\//i, + /e2e\//, + ] + if (testPatterns.some((p) => p.test(filePath))) { + return false + } + } + + // Check file pattern if provided + if (config.diffFilePattern) { + // eslint-disable-next-line security/detect-non-literal-regexp -- User-provided pattern from config, used for file filtering + const pattern = new RegExp(config.diffFilePattern) + if (!pattern.test(filePath)) { + return false + } + } else { + // Default pattern: TypeScript and JavaScript files + const defaultPattern = /\.(ts|tsx|js|jsx)$/ + if (!defaultPattern.test(filePath)) { + return false + } + } + + return true + } + + /** + * Collect diff for a specific file in a commit + */ + private async collectFileDiff(sha: string, filePath: string, config: CommitDataConfig): Promise { + try { + // Get just the unified diff for this file (more compact than full git show) + const diff = await $`git -C ${this.repoPath} diff ${sha}^..${sha} -- ${filePath}`.text() + + if (!diff || diff.trim().length === 0) { + // File might be new, try different approach + const showDiff = await $`git -C ${this.repoPath} show ${sha} --format= -- ${filePath}`.text() + if (!showDiff) { + return undefined + } + + // For new files, just get the content preview + const lines = showDiff.split('\n') + const maxSize = Math.min(config.maxDiffSize || 100, 30) // Smaller preview for new files + if (lines.length > maxSize) { + return lines.slice(0, maxSize).join('\n') + `\n... [+${lines.length - maxSize} more lines]` + } + return showDiff + } + + // Extract just the hunks (skip file headers for compactness) + const lines = diff.split('\n') + const hunkLines = lines.filter( + (line: string) => line.startsWith('@@') || line.startsWith('+') || line.startsWith('-') || line.startsWith(' '), + ) + + // Check size and truncate if needed + const maxSize = config.maxDiffSize || 100 + if (hunkLines.length > maxSize) { + const truncated = hunkLines.slice(0, maxSize) + truncated.push(`... [${hunkLines.length - maxSize} more lines]`) + return truncated.join('\n') + } + + return hunkLines.join('\n') + } catch (_error) { + // Might fail for new files or first commit, that's ok + return undefined + } + } + + /** + * Restore Date objects from cached JSON (JSON.parse converts dates to strings) + */ + private restoreDatesFromCache(items: T[]): T[] { + return items.map((item) => { + if (item.timestamp && typeof item.timestamp === 'string') { + return { ...item, timestamp: new Date(item.timestamp) } + } + return item + }) + } + + async collect(options: CollectOptions): Promise { + this.logger?.info('Collecting repository data...') + + // Reset filtering stats + this.filteringStats = { + totalCommitsFound: 0, + trivialCommitsFiltered: 0, + filesProcessed: 0, + trivialFilesSkipped: 0, + } + this.tokenUsage = 0 + this.diffsCollected = 0 + + // Try to get commits from cache + let allCommits: Commit[] | null = null + if (!this.bypassCache && this.cacheProvider) { + const cacheKey = getCommitsCacheKey(options) + const cached = await this.cacheProvider.get(cacheKey) + if (cached) { + allCommits = this.restoreDatesFromCache(cached) + this.logger?.info(`Cache hit: Found ${allCommits.length} commits in cache`) + } + } + + // Fetch commits if not cached + if (!allCommits) { + if (options.releaseComparison) { + this.logger?.info(`Fetching commits for release: ${options.releaseComparison.commitRange}`) + allCommits = await this.getReleaseCommits(options.releaseComparison, options) + } else { + this.logger?.info('Fetching commits from git log...') + allCommits = await this.getCommits(options) + } + + // Store in cache (release comparisons are deterministic, cache indefinitely) + if (this.cacheProvider) { + const cacheKey = getCommitsCacheKey(options) + const ttl = options.releaseComparison ? undefined : 3600 // 1 hour for time-based queries + await this.cacheProvider.set(cacheKey, allCommits, ttl) + this.logger?.info(`Cached ${allCommits.length} commits`) + } + } + + // Filter by team if specified + const commits = options.teamFilter?.length ? this.filterCommitsByTeam(allCommits, options.teamFilter) : allCommits + + this.logger?.info(`Found ${commits.length} commits (${allCommits.length} total)`) + + // Try to get PRs from cache + let pullRequests: PullRequest[] | null = null + if (!this.bypassCache && this.cacheProvider) { + const cacheKey = getPullRequestsCacheKey(options) + const cached = await this.cacheProvider.get(cacheKey) + if (cached) { + pullRequests = cached + this.logger?.info(`Cache hit: Found ${pullRequests.length} PRs in cache`) + } + } + + // Fetch PRs if not cached + if (!pullRequests) { + this.logger?.info('Fetching pull requests from GitHub...') + pullRequests = await this.getPullRequests(options) + + // Store in cache + if (this.cacheProvider) { + const cacheKey = getPullRequestsCacheKey(options) + const ttl = options.releaseComparison ? undefined : 3600 // 1 hour for time-based queries + await this.cacheProvider.set(cacheKey, pullRequests, ttl) + this.logger?.info(`Cached ${pullRequests.length} PRs`) + } + } + + this.logger?.info(`Found ${pullRequests.length} pull requests`) + + // Try to get stats from cache + let stats: StatsOutput | null = null + if (!this.bypassCache && this.cacheProvider) { + const cacheKey = getStatsCacheKey(options) + const cached = await this.cacheProvider.get(cacheKey) + if (cached) { + stats = cached + this.logger?.info('Cache hit: Found stats in cache') + } + } + + // Calculate stats if not cached + if (!stats) { + stats = await this.getStats(options) + + // Store in cache + if (this.cacheProvider) { + const cacheKey = getStatsCacheKey(options) + const ttl = options.releaseComparison ? undefined : 3600 // 1 hour for time-based queries + await this.cacheProvider.set(cacheKey, stats, ttl) + this.logger?.info('Cached stats') + } + } + + // Build metadata + const repoName = + options.repository?.owner && options.repository.name + ? `${options.repository.owner}/${options.repository.name}` + : this.repoPath + + const metadata: RepositoryData['metadata'] = { + repository: repoName, + period: options.since, + collectedAt: new Date(), + commitCount: commits.length, + prCount: pullRequests.length, + } + + // Add release info if available + if (options.releaseComparison) { + metadata.releaseInfo = { + from: options.releaseComparison.from.version, + to: options.releaseComparison.to.version, + platform: options.releaseComparison.to.platform, + } + } + + // Add filtering stats if we have them + if (this.filteringStats.totalCommitsFound > 0) { + metadata.filtering = { + totalCommitsFound: this.filteringStats.totalCommitsFound, + trivialCommitsFiltered: this.filteringStats.trivialCommitsFiltered, + filesProcessed: this.filteringStats.filesProcessed, + trivialFilesSkipped: this.filteringStats.trivialFilesSkipped, + } + } + + return { + commits, + pullRequests, + stats, + metadata, + } + } + + private async getCommits(options: CollectOptions): Promise { + const format = '%H|%ae|%an|%at|%s' + const result = await $`git -C ${this.repoPath} log --since="${options.since}" --format="${format}" --numstat`.text() + + const commits: Commit[] = [] + const lines = result.split('\n') + let skippedTrivialCommits = 0 + let totalCommitsProcessed = 0 + + for (let i = 0; i < lines.length; i++) { + const line = lines[i] + if (!line || !line.includes('|')) { + continue + } + + // Parse the commit line + const parsed = parseGitLogLine(line) + if (!parsed) { + this.logger?.warn(`Skipping malformed git log line: ${line}`) + continue + } + + totalCommitsProcessed++ + const { sha, email, name, timestamp, message } = parsed + const stats = { filesChanged: 0, insertions: 0, deletions: 0 } + const fileChanges: Commit['files'] = [] + + // Parse numstat + i++ + while (i < lines.length) { + const currentLine = lines[i] + if (!currentLine || currentLine.includes('|')) { + break + } + const statLine = currentLine.trim() + if (statLine) { + const numstat = parseNumstatLine(statLine) + if (numstat) { + const { additions, deletions, filepath } = numstat + + // Skip trivial files when in release mode + if (options.releaseComparison && isTrivialFile(filepath)) { + i++ + continue + } + + const adds = additions === '-' ? 0 : parseInt(additions, 10) || 0 + const dels = deletions === '-' ? 0 : parseInt(deletions, 10) || 0 + + // Determine file status + let status: 'added' | 'modified' | 'deleted' | 'renamed' = 'modified' + if (adds > 0 && dels === 0) { + status = 'added' + } else if (adds === 0 && dels > 0) { + status = 'deleted' + } else if (additions === '-' || deletions === '-') { + status = 'modified' + } + + fileChanges.push({ + path: filepath, + status, + additions: adds, + deletions: dels, + }) + + stats.insertions += adds + stats.deletions += dels + stats.filesChanged++ + } + } + i++ + } + i-- // Back up one since the outer loop will increment + + // Skip commits that only touch trivial files (only in release mode) + if (options.releaseComparison && stats.filesChanged === 0) { + skippedTrivialCommits++ + continue + } + + const commit = { + sha, + author: { name, email }, + timestamp: new Date(parseInt(timestamp, 10) * 1000), + message, + stats, + files: fileChanges, + } + + // Collect diffs if configured + if (options.commitDataConfig?.includeDiffs && fileChanges.length > 0) { + for (const file of fileChanges) { + if (this.shouldIncludeDiff(file.path, file.additions, file.deletions, options.commitDataConfig)) { + const diff = await this.collectFileDiff(sha, file.path, options.commitDataConfig) + if (diff) { + file.diff = diff + file.diffTruncated = diff.includes('[diff truncated') + this.diffsCollected++ + this.tokenUsage += this.estimateTokens(diff) + + // Log progress + if (this.diffsCollected % 5 === 0) { + this.logger?.info(`Collected ${this.diffsCollected} diffs (${this.tokenUsage} tokens used)`) + } + } + } + } + } + + commits.push(commit) + } + + // Store stats for reporting (if in release mode) + if (totalCommitsProcessed > 0) { + this.filteringStats.totalCommitsFound += totalCommitsProcessed + this.filteringStats.trivialCommitsFiltered += skippedTrivialCommits + } + + if (skippedTrivialCommits > 0) { + this.logger?.info(`Filtered out ${skippedTrivialCommits} commits with only trivial file changes`) + } + + return commits + } + + private filterCommitsByTeam(commits: Commit[], teamFilter: string[]): Commit[] { + return commits.filter((c) => { + const matches = teamFilter.includes(c.author.email) + if (!matches) { + this.logger?.debug(`Filtering out commit ${c.sha.slice(0, 7)} by ${c.author.email}`) + } + return matches + }) + } + + private async getPullRequests(options: CollectOptions): Promise { + if (!options.repository?.owner || !options.repository.name) { + this.logger?.debug('No repository configured, skipping PR fetch') + return [] + } + + const repository = `${options.repository.owner}/${options.repository.name}` + + try { + // For release comparisons, get PR numbers from the commit range + if (options.releaseComparison) { + return await this.getReleasePullRequests(options, repository) + } + + // Parse the 'since' date for time-based analysis + const sinceDate = this.parseSinceDate(options.since) + const sinceISO = sinceDate.toISOString().split('T')[0] ?? sinceDate.toISOString() + + // Build search query + const authorFilter = options.teamUsernames?.length + ? options.teamUsernames.map((author) => `author:${author}`).join(' ') + : '' + + const query = options.includeOpenPrs + ? `repo:${repository} is:pr created:>=${sinceISO} ${authorFilter}` + : `repo:${repository} is:pr is:closed closed:>=${sinceISO} ${authorFilter}` + + this.logger?.debug(`GitHub Search Query: ${query}`) + + // Get PR numbers from main branch commits for filtering + const prNumbersInMain = await this.getPRNumbersFromMainBranch(sinceISO) + + // Fetch PRs from GitHub + const allPRs: PullRequest[] = [] + let page = 1 + const perPage = 100 + const maxPages = 10 + + while (page <= maxPages) { + const apiPath = `/search/issues?q=${encodeURIComponent(query)}&per_page=${perPage}&page=${page}` + const searchResult = await $`gh api ${apiPath}`.text() + const searchData = JSON.parse(searchResult) + + if (!searchData.items || searchData.items.length === 0) { + break + } + + for (const pr of searchData.items) { + // Filter to only include PRs whose commits are in main (unless open) + if (pr.state === 'open' || prNumbersInMain.has(pr.number)) { + const limit = options.commitDataConfig?.prBodyLimit || 2000 + const shouldClean = options.commitDataConfig?.cleanPRBodies !== false // Default to true + let body = pr.body ? pr.body : '' + + // Clean PR body if enabled (default: true) + if (body && shouldClean) { + body = cleanPRBody(body) + } + + // Apply character limit after cleaning + body = body.slice(0, limit) + + allPRs.push({ + number: pr.number, + title: pr.title, + body, + author: pr.user?.login || 'unknown', + state: pr.state as 'open' | 'closed', + mergedAt: pr.closed_at, + mergeCommitSha: pr.pull_request?.merge_commit_sha, + }) + } + } + + if (searchData.items.length < perPage) { + break + } + page++ + } + + return allPRs + } catch (error) { + this.logger?.error(`GitHub PR fetch failed: ${error}`) + return [] + } + } + + private parseSinceDate(since: string): Date { + const sinceDate = new Date() + const sinceMatch = since.match(/(\d+)\s+(day|week|month|year)s?\s+ago/) + + if (sinceMatch?.[1] && sinceMatch[2]) { + const amount = sinceMatch[1] + const unit = sinceMatch[2] + const num = parseInt(amount, 10) + + switch (unit) { + case 'day': + sinceDate.setDate(sinceDate.getDate() - num) + break + case 'week': + sinceDate.setDate(sinceDate.getDate() - num * 7) + break + case 'month': + sinceDate.setMonth(sinceDate.getMonth() - num) + break + case 'year': + sinceDate.setFullYear(sinceDate.getFullYear() - num) + break + } + } + + return sinceDate + } + + private async getPRNumbersFromMainBranch(sinceISO: string): Promise> { + const allCommitMessages = await $`git log main --since="${sinceISO}" --format="%s"`.text() + const prNumbersInMain = new Set() + const prRegex = /#(\d+)/g + + for (const match of allCommitMessages.matchAll(prRegex)) { + if (match[1]) { + const prNum = parseInt(match[1], 10) + if (prNum) { + prNumbersInMain.add(prNum) + } + } + } + + this.logger?.debug(`Found ${prNumbersInMain.size} unique PR numbers in main branch commits`) + return prNumbersInMain + } + + private async getReleasePullRequests(options: CollectOptions, repository: string): Promise { + if (!options.releaseComparison) { + return [] + } + + const range = options.releaseComparison.commitRange + this.logger?.info('Extracting PR information from commits...') + + // Extract PR numbers and titles from commit messages + const commits = await $`git -C ${this.repoPath} log ${range} --format="%H|%s|%ae|%an"`.text() + const pullRequests: PullRequest[] = [] + const seenPRs = new Set() + + for (const line of commits.split('\n')) { + if (!line) { + continue + } + const parts = line.split('|') + const sha = parts[0] + const message = parts[1] + const author = parts[3] + + if (!sha || !message || !author) { + continue + } + + // Look for PR number in commit message (e.g., "(#1234)" or "PR #1234") + const prMatch = message.match(/#(\d+)/) + if (prMatch?.[1]) { + const prNumber = parseInt(prMatch[1], 10) + + // Skip if we've already seen this PR + if (seenPRs.has(prNumber)) { + continue + } + seenPRs.add(prNumber) + + // Extract PR title from commit message (usually after the PR number) + let title = message + // Remove PR number patterns + title = title + .replace(/\(#\d+\)/, '') + .replace(/#\d+/, '') + .trim() + + // Create a minimal PR object from commit data + pullRequests.push({ + number: prNumber, + title: title || `PR #${prNumber}`, + body: '', // We don't have the body without API call + author: author || 'unknown', + state: 'closed', // Assume closed if in release + mergedAt: '', // We don't have exact merge time + mergeCommitSha: sha, + }) + } + } + + this.logger?.info(`Found ${pullRequests.length} PRs from commit messages`) + + // Fetch detailed PR info for all PRs using gh api with parallel batching + if (pullRequests.length > 0) { + this.logger?.info(`Fetching detailed info for ${pullRequests.length} PRs...`) + + const limit = options.commitDataConfig?.prBodyLimit || 2000 + const CONCURRENCY_LIMIT = 15 // Fetch 15 PRs in parallel at a time + let fetchedCount = 0 + + // Helper function to fetch a single PR + const fetchPR = async (pr: PullRequest): Promise => { + try { + // Use jq to output a JSON object to properly handle multi-line PR bodies + const prResultJson = + await $`gh api repos/${repository}/pulls/${pr.number} --jq '{title: .title, body: .body, author: .user.login, mergedAt: .merged_at}'`.text() + const prData = JSON.parse(prResultJson) + + // Update with real data + if (prData.title) { + pr.title = prData.title + } + if (prData.body && prData.body !== 'null') { + const shouldClean = options.commitDataConfig?.cleanPRBodies !== false // Default to true + let body = prData.body + + // Clean PR body if enabled (default: true) + if (shouldClean) { + body = cleanPRBody(body) + } + + // Apply character limit after cleaning + pr.body = body.slice(0, limit) + } + if (prData.author) { + pr.author = prData.author + } + if (prData.mergedAt && prData.mergedAt !== 'null') { + pr.mergedAt = prData.mergedAt + } + } catch (_error) { + // Keep the minimal data we already have + this.logger?.warn(`Failed to fetch PR #${pr.number}, continuing with minimal data`) + } + } + + // Process PRs in batches for parallel fetching + for (let i = 0; i < pullRequests.length; i += CONCURRENCY_LIMIT) { + const batch = pullRequests.slice(i, i + CONCURRENCY_LIMIT) + const batchNumber = Math.floor(i / CONCURRENCY_LIMIT) + 1 + const totalBatches = Math.ceil(pullRequests.length / CONCURRENCY_LIMIT) + + // Fetch all PRs in this batch in parallel + await Promise.all(batch.map((pr) => fetchPR(pr))) + + fetchedCount += batch.length + + // Log progress after each batch + this.logger?.info( + `Fetched ${fetchedCount}/${pullRequests.length} PRs (batch ${batchNumber}/${totalBatches})...`, + ) + + // Add small delay between batches to respect rate limits (except for last batch) + if (i + CONCURRENCY_LIMIT < pullRequests.length) { + await new Promise((resolve) => setTimeout(resolve, 200)) + } + } + + this.logger?.info(`Successfully fetched detailed info for ${fetchedCount}/${pullRequests.length} PRs`) + } + + return pullRequests + } + + private async getReleaseCommits(comparison: ReleaseComparison, options: CollectOptions): Promise { + const format = '%H|%ae|%an|%at|%s' + const range = comparison.commitRange + + this.logger?.info(`Getting commits for release: ${range}`) + + const result = await $`git -C ${this.repoPath} log ${range} --format="${format}" --numstat`.text() + + const commits: Commit[] = [] + const lines = result.split('\n') + let skippedTrivialCommits = 0 + let totalCommitsProcessed = 0 + let totalFilesProcessed = 0 + let trivialFilesSkipped = 0 + + for (let i = 0; i < lines.length; i++) { + const line = lines[i] + if (!line || !line.includes('|')) { + continue + } + + // Parse the commit line + const parsed = parseGitLogLine(line) + if (!parsed) { + this.logger?.warn(`Skipping malformed git log line: ${line}`) + continue + } + + totalCommitsProcessed++ + const { sha, email, name, timestamp, message } = parsed + const stats = { filesChanged: 0, insertions: 0, deletions: 0 } + const fileChanges: Commit['files'] = [] + const trivialFiles: string[] = [] + + // Parse numstat + i++ + // Skip empty line after commit header + if (i < lines.length && lines[i] === '') { + i++ + } + + while (i < lines.length) { + const statLine = lines[i] + + // Stop if this is the start of a new commit (contains |) or we hit another empty line followed by a commit + if (!statLine || statLine.includes('|')) { + if (!statLine) { + // Check if next line is a commit + const nextLine = lines[i + 1] + if (nextLine?.includes('|')) { + break + } + // Otherwise it's just an empty line in the numstat, skip it + i++ + continue + } else { + // New commit line, back up for outer loop + i-- + break + } + } + + if (statLine.trim()) { + // Git numstat format is: additions\tdeletions\tfilepath + const tabIndex1 = statLine.indexOf('\t') + const tabIndex2 = statLine.indexOf('\t', tabIndex1 + 1) + + if (tabIndex1 > -1 && tabIndex2 > -1) { + const additions = statLine.substring(0, tabIndex1) + const deletions = statLine.substring(tabIndex1 + 1, tabIndex2) + const filepath = statLine.substring(tabIndex2 + 1) + + // Skip empty filepaths + if (!filepath) { + i++ + continue + } + + totalFilesProcessed++ + + // Skip trivial files + if (!isTrivialFile(filepath)) { + const adds = additions === '-' ? 0 : parseInt(additions, 10) || 0 + const dels = deletions === '-' ? 0 : parseInt(deletions, 10) || 0 + + // Determine file status based on additions/deletions + let status: 'added' | 'modified' | 'deleted' | 'renamed' = 'modified' + if (adds > 0 && dels === 0) { + status = 'added' + } else if (adds === 0 && dels > 0) { + status = 'deleted' + } else if (additions === '-' || deletions === '-') { + // Binary files or renames show '-' for stats + status = 'modified' + } + + fileChanges.push({ + path: filepath, + status, + additions: adds, + deletions: dels, + }) + + stats.insertions += adds + stats.deletions += dels + stats.filesChanged++ + } else { + trivialFiles.push(filepath) + trivialFilesSkipped++ + } + } + } + i++ + } + i-- // Back up one since the outer loop will increment + + // Skip commits that only touch trivial files + if (stats.filesChanged === 0 && trivialFiles.length > 0) { + skippedTrivialCommits++ + continue + } else if (stats.filesChanged === 0 && trivialFiles.length === 0) { + // Empty commit or merge commit, skip + continue + } + + const commit = { + sha, + author: { name, email }, + timestamp: new Date(parseInt(timestamp, 10) * 1000), + message, + stats, + files: fileChanges, + } + + // Collect diffs if configured + if (options.commitDataConfig?.includeDiffs && fileChanges.length > 0) { + for (const file of fileChanges) { + if (this.shouldIncludeDiff(file.path, file.additions, file.deletions, options.commitDataConfig)) { + const diff = await this.collectFileDiff(sha, file.path, options.commitDataConfig) + if (diff) { + file.diff = diff + file.diffTruncated = diff.includes('[diff truncated') + this.diffsCollected++ + this.tokenUsage += this.estimateTokens(diff) + + // Log progress + if (this.diffsCollected % 5 === 0) { + this.logger?.info(`Collected ${this.diffsCollected} diffs (${this.tokenUsage} tokens used)`) + } + } + } + } + } + + commits.push(commit) + } + + // Store stats for reporting (if in release mode) + if (totalCommitsProcessed > 0) { + this.filteringStats.totalCommitsFound = totalCommitsProcessed + this.filteringStats.trivialCommitsFiltered = skippedTrivialCommits + this.filteringStats.filesProcessed = totalFilesProcessed + this.filteringStats.trivialFilesSkipped = trivialFilesSkipped + } + + if (skippedTrivialCommits > 0) { + this.logger?.info(`Filtered out ${skippedTrivialCommits} commits with only trivial file changes`) + } + + this.logger?.info(`Analyzed ${commits.length} meaningful commits from ${totalCommitsProcessed} total`) + + if (options.commitDataConfig?.includeDiffs && this.diffsCollected > 0) { + this.logger?.info(`Collected ${this.diffsCollected} diffs using ~${this.tokenUsage.toLocaleString()} tokens`) + } + + return commits + } + + private async getStats(options: CollectOptions): Promise { + // For release comparisons, use the commit range + if (options.releaseComparison) { + const range = options.releaseComparison.commitRange + const shortstat = await $`git -C ${this.repoPath} log ${range} --shortstat --format=""`.text() + const authors = await $`git -C ${this.repoPath} log ${range} --format="%ae" | sort -u | wc -l`.text() + + let filesChanged = 0, + linesAdded = 0, + linesDeleted = 0, + totalCommits = 0 + + for (const line of shortstat.split('\n')) { + if (line.includes('changed')) { + totalCommits++ + const fileMatch = line.match(/(\d+) file/) + const insertMatch = line.match(/(\d+) insertion/) + const deleteMatch = line.match(/(\d+) deletion/) + + if (fileMatch?.[1]) { + filesChanged += parseInt(fileMatch[1], 10) + } + if (insertMatch?.[1]) { + linesAdded += parseInt(insertMatch[1], 10) + } + if (deleteMatch?.[1]) { + linesDeleted += parseInt(deleteMatch[1], 10) + } + } + } + + return { + totalCommits, + totalAuthors: parseInt(authors.trim(), 10), + filesChanged, + linesAdded, + linesDeleted, + } + } + + // Original implementation for time-based analysis + const shortstat = await $`git -C ${this.repoPath} log --since="${options.since}" --shortstat --format=""`.text() + const authors = + await $`git -C ${this.repoPath} log --since="${options.since}" --format="%ae" | sort -u | wc -l`.text() + + let filesChanged = 0, + linesAdded = 0, + linesDeleted = 0, + totalCommits = 0 + + for (const line of shortstat.split('\n')) { + if (line.includes('changed')) { + totalCommits++ + const fileMatch = line.match(/(\d+) file/) + const insertMatch = line.match(/(\d+) insertion/) + const deleteMatch = line.match(/(\d+) deletion/) + + if (fileMatch?.[1]) { + filesChanged += parseInt(fileMatch[1], 10) + } + if (insertMatch?.[1]) { + linesAdded += parseInt(insertMatch[1], 10) + } + if (deleteMatch?.[1]) { + linesDeleted += parseInt(deleteMatch[1], 10) + } + } + } + + return { + totalCommits, + totalAuthors: parseInt(authors.trim(), 10), + filesChanged, + linesAdded, + linesDeleted, + } + } +} diff --git a/apps/cli/src/core/orchestrator.ts b/apps/cli/src/core/orchestrator.ts new file mode 100644 index 00000000000..a72effbcf42 --- /dev/null +++ b/apps/cli/src/core/orchestrator.ts @@ -0,0 +1,810 @@ +/* eslint-disable max-depth */ +/* eslint-disable security/detect-non-literal-regexp */ +/* eslint-disable complexity */ +/* eslint-disable max-lines */ +/* eslint-disable no-console */ +import { join } from 'node:path' +import { + type CollectOptions, + DataCollector, + type PullRequest, + type RepositoryData, +} from '@universe/cli/src/core/data-collector' +import type { AIProvider } from '@universe/cli/src/lib/ai-provider' +import { AnalysisWriter } from '@universe/cli/src/lib/analysis-writer' +import type { CacheProvider } from '@universe/cli/src/lib/cache-provider' +import type { Logger } from '@universe/cli/src/lib/logger' +import { ReleaseScanner } from '@universe/cli/src/lib/release-scanner' + +// ============================================================================ +// Types +// ============================================================================ + +export interface AnalysisConfig { + mode?: string // Predefined mode (team-digest, changelog, release-changelog, etc.) + prompt?: string // Custom prompt (file path or inline text) + promptFile?: string // Explicit prompt file path + variables?: Record // Variable substitution for prompts + releaseOptions?: { + // Release-specific options + platform: 'mobile' | 'extension' + version: string + compareWith?: string + } +} + +export interface OutputConfig { + type: string // Output type (slack, markdown, file, etc.) + target?: string // Target destination (file path, channel, etc.) + options?: Record // Type-specific options +} + +export interface OrchestratorConfig { + analysis: AnalysisConfig + outputs: OutputConfig[] + collect: CollectOptions + verbose?: boolean + dryRun?: boolean + saveArtifacts?: boolean + model?: string // AI model to use (defaults to claude-opus-4-1-20250805) + bypassCache?: boolean // Bypass cache for this run +} + +// ============================================================================ +// Prompt Management +// ============================================================================ + +class PromptResolver { + private builtInPromptsPath = join((import.meta.dir as string | undefined) ?? process.cwd(), 'src', 'prompts') + private projectPromptsPath = '.claude/prompts' + + async resolve(promptRef: string): Promise { + // If it's a multiline string or looks like instructions, use as-is + if (promptRef.includes('\n') || promptRef.length > 100) { + return promptRef + } + + // If it ends with .md, treat as file path + if (promptRef.endsWith('.md')) { + return await this.loadFromFile(promptRef) + } + + // Check for built-in prompts + const builtInPath = join(this.builtInPromptsPath, `${promptRef}.md`) + if (await this.fileExists(builtInPath)) { + return await this.loadFromFile(builtInPath) + } + + // Check for project prompts + const projectPath = join(this.projectPromptsPath, `${promptRef}.md`) + if (await this.fileExists(projectPath)) { + return await this.loadFromFile(projectPath) + } + + // Treat as inline prompt if not found as file + return promptRef + } + + private async fileExists(path: string): Promise { + try { + await Bun.file(path).text() + return true + } catch { + return false + } + } + + private async loadFromFile(path: string): Promise { + try { + return await Bun.file(path).text() + } catch (error) { + throw new Error(`Failed to load prompt from ${path}: ${error}`) + } + } + + substituteVariables(prompt: string, variables: Record): string { + let result = prompt + for (const [key, value] of Object.entries(variables)) { + const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + // Use string replace with regex pattern - escapedKey is safe as it's escaped + const dollarPattern = new RegExp(`\\$${escapedKey}`, 'g') + const bracePattern = new RegExp(`{{${escapedKey}}}`, 'g') + result = result.replace(dollarPattern, value) + result = result.replace(bracePattern, value) + } + return result + } +} + +// ============================================================================ +// Analysis Orchestrator +// ============================================================================ + +export interface OrchestratorDependencies { + config: OrchestratorConfig + aiProvider: AIProvider + cacheProvider?: CacheProvider + logger: Logger +} + +export class Orchestrator { + private promptResolver = new PromptResolver() + private dataCollector: DataCollector + private analysisWriter: AnalysisWriter + private startTime: number = 0 + private timings: { dataCollection: number; claudeAnalysis: number } = { dataCollection: 0, claudeAnalysis: 0 } + private config: OrchestratorConfig + private aiProvider: AIProvider + private logger: Logger + + constructor(deps: OrchestratorDependencies) { + this.config = deps.config + this.aiProvider = deps.aiProvider + this.logger = deps.logger + this.dataCollector = new DataCollector( + deps.config.collect.repoPath, + deps.cacheProvider, + deps.config.bypassCache || false, + deps.logger, + ) + this.analysisWriter = new AnalysisWriter() + } + + async execute(): Promise> { + this.startTime = Date.now() + const runId = this.analysisWriter.getRunId() + this.logger.info(`Starting repository analysis${this.config.saveArtifacts ? ` (run: ${runId})` : ''}`) + + // Initialize analysis directory if saving artifacts + if (this.config.saveArtifacts) { + await this.analysisWriter.initialize() + // Save configuration + await this.analysisWriter.saveConfig(this.config) + } + + // Step 1: Collect repository data + const dataStartTime = Date.now() + const data = await this.collectData() + this.timings.dataCollection = Date.now() - dataStartTime + + // Step 2: Run analysis with universal analyzer + const analysisStartTime = Date.now() + const insights = await this.analyze(data) + this.timings.claudeAnalysis = Date.now() - analysisStartTime + + // Step 3: Deliver to outputs + await this.deliver(insights, data) + + // Generate summary + await this.generateSummary(data) + + const totalTime = Date.now() - this.startTime + this.logger.info(`Analysis complete! (${(totalTime / 1000).toFixed(1)}s)`) + this.logger.info(`View artifacts: ${this.analysisWriter.getRunPath()}/`) + + // Return the analysis results for UI consumption + return insights + } + + private async collectData(): Promise { + this.logger.info('Collecting repository data...') + + let data: RepositoryData + + // If in release mode (release-changelog or bug-bisect), augment collect options with release comparison + if ( + (this.config.analysis.mode === 'release-changelog' || this.config.analysis.mode === 'bug-bisect') && + this.config.analysis.releaseOptions + ) { + const scanner = new ReleaseScanner(this.config.collect.repoPath, this.logger) + const { platform, version, compareWith } = this.config.analysis.releaseOptions + + const comparison = await scanner.getReleaseComparison({ + platform, + version, + compareWith, + }) + if (!comparison) { + throw new Error(`Could not find release comparison for ${platform}/${version}`) + } + + this.logger.info(`Analyzing release: ${platform}/${version} (comparing with ${comparison.from.version})`) + + // Add release comparison to collect options + const collectOptions: CollectOptions = { + ...this.config.collect, + releaseComparison: comparison, + } + + data = await this.dataCollector.collect(collectOptions) + } else { + data = await this.dataCollector.collect(this.config.collect) + } + + // Save collected data if saving artifacts + if (this.config.saveArtifacts) { + await this.analysisWriter.saveCommits(data.commits, data.metadata) + await this.analysisWriter.savePullRequests(data.pullRequests) + await this.analysisWriter.saveStats(data.stats) + } + + return data + } + + private async analyze(data: RepositoryData): Promise> { + this.logger.info('Running analysis...') + + // Build the analysis prompt + const prompt = await this.buildPrompt(data) + if (this.config.saveArtifacts) { + await this.analysisWriter.savePrompt(prompt) + } + + // Prepare data context + const context = this.prepareContext(data) + if (this.config.saveArtifacts) { + await this.analysisWriter.saveContext(context) + } + + // Smart injection: replace if template has placeholder, otherwise append + let analysisPrompt: string + if (prompt.includes('{{COMMIT_DATA}}')) { + // New style: Replace the variable + analysisPrompt = prompt.replace(/{{COMMIT_DATA}}/g, context) + } else { + // Legacy style: Append to end + analysisPrompt = `${prompt}\n\n## Repository Data\n\n${context}` + } + + // Estimate tokens (rough approximation: 1 token ≈ 4 characters) + let estimatedTokens = Math.round(analysisPrompt.length / 4) + this.logger.info(`Prepared Claude context (estimated ~${estimatedTokens.toLocaleString()} tokens)...`) + + // Check if we're over Claude's limit (roughly 200k tokens for Claude 3) + const MAX_TOKENS = 150000 // Conservative limit to leave room for response + + if (estimatedTokens > MAX_TOKENS) { + this.logger.warn(`Context too large (${estimatedTokens} tokens), reducing data...`) + + // Step 1: Try again without diffs (prefer PR bodies over diffs) + const reducedContext = this.prepareContext(data, true) // skipDiffs flag + if (prompt.includes('{{COMMIT_DATA}}')) { + analysisPrompt = prompt.replace(/{{COMMIT_DATA}}/g, reducedContext) + } else { + analysisPrompt = `${prompt}\n\n## Repository Data\n\n${reducedContext}` + } + estimatedTokens = Math.round(analysisPrompt.length / 4) + this.logger.info(`Reduced context to ~${estimatedTokens.toLocaleString()} tokens (removed diffs)`) + + // Step 2: If still too large, truncate PR bodies proportionally + if (estimatedTokens > MAX_TOKENS) { + this.logger.warn('Still too large, truncating PR bodies...') + const dataWithTruncatedPRs = this.truncatePRBodies(data, 0.5) // Reduce to 50% of original length + const contextWithTruncatedPRs = this.prepareContext(dataWithTruncatedPRs, true) + if (prompt.includes('{{COMMIT_DATA}}')) { + analysisPrompt = prompt.replace(/{{COMMIT_DATA}}/g, contextWithTruncatedPRs) + } else { + analysisPrompt = `${prompt}\n\n## Repository Data\n\n${contextWithTruncatedPRs}` + } + estimatedTokens = Math.round(analysisPrompt.length / 4) + this.logger.info(`Reduced context to ~${estimatedTokens.toLocaleString()} tokens (truncated PR bodies to 50%)`) + + // Step 3: If still too large, truncate commits + if (estimatedTokens > MAX_TOKENS) { + this.logger.warn('Still too large, truncating commit list...') + const truncatedData = { ...dataWithTruncatedPRs, commits: dataWithTruncatedPRs.commits.slice(0, 100) } + const minimalContext = this.prepareContext(truncatedData, true) + if (prompt.includes('{{COMMIT_DATA}}')) { + analysisPrompt = prompt.replace(/{{COMMIT_DATA}}/g, minimalContext) + } else { + analysisPrompt = `${prompt}\n\n## Repository Data\n\n${minimalContext}` + } + estimatedTokens = Math.round(analysisPrompt.length / 4) + this.logger.info(`Final context: ~${estimatedTokens.toLocaleString()} tokens (kept first 100 commits)`) + } + } + } + + if (this.config.saveArtifacts) { + await this.analysisWriter.saveClaudeInput(analysisPrompt) + } + + if (this.config.verbose) { + this.logger.debug(`Analysis prompt: ${analysisPrompt.slice(0, 500)}...`) + } + + // For now, directly use Claude API since Task is not available in this context + // In production, this would use the Task API + const result = await this.analyzeWithClaude(analysisPrompt) + + // Save Claude's output + if (this.config.saveArtifacts) { + await this.analysisWriter.saveClaudeOutput(result) + } + + return result + } + + private async buildPrompt(data: RepositoryData): Promise { + let promptText = '' + + // Load base prompt + if (this.config.analysis.mode) { + promptText = await this.promptResolver.resolve(this.config.analysis.mode) + } else if (this.config.analysis.promptFile) { + promptText = await this.promptResolver.resolve(this.config.analysis.promptFile) + } else if (this.config.analysis.prompt) { + promptText = await this.promptResolver.resolve(this.config.analysis.prompt) + } else { + // Default to team-digest + promptText = await this.promptResolver.resolve('team-digest') + } + + // Build variables for substitution + const variables: Record = { + ...this.config.analysis.variables, + } + + // Add release context variables for bug-bisect mode + if ( + this.config.analysis.mode === 'bug-bisect' && + this.config.analysis.releaseOptions && + data.metadata.releaseInfo + ) { + variables.PLATFORM = data.metadata.releaseInfo.platform + variables.RELEASE_TO = data.metadata.releaseInfo.to + variables.RELEASE_FROM = data.metadata.releaseInfo.from + } + + // Substitute variables if provided + if (Object.keys(variables).length > 0) { + promptText = this.promptResolver.substituteVariables(promptText, variables) + } + + return promptText + } + + /** + * Truncate PR bodies proportionally to reduce context size + * @param data Original repository data + * @param ratio Ratio to keep (0.5 = keep 50% of each PR body) + */ + private truncatePRBodies(data: RepositoryData, ratio: number): RepositoryData { + const truncatedPRs = data.pullRequests.map((pr: PullRequest) => { + if (pr.body && pr.body.length > 0) { + const targetLength = Math.max(100, Math.floor(pr.body.length * ratio)) // Keep at least 100 chars + const truncatedBody = pr.body.slice(0, targetLength) + (pr.body.length > targetLength ? '... [truncated]' : '') + return { + number: pr.number, + title: pr.title, + body: truncatedBody, + author: pr.author, + state: pr.state, + mergedAt: pr.mergedAt, + mergeCommitSha: pr.mergeCommitSha, + } satisfies PullRequest + } + return { + number: pr.number, + title: pr.title, + body: pr.body, + author: pr.author, + state: pr.state, + mergedAt: pr.mergedAt, + mergeCommitSha: pr.mergeCommitSha, + } satisfies PullRequest + }) as PullRequest[] + + return { + ...data, + pullRequests: truncatedPRs, + } + } + + private prepareContext(data: RepositoryData, skipDiffs: boolean = false): string { + const lines: string[] = [] + let prBodyTokens = 0 + + // Metadata section (compact format) + lines.push('=== REPOSITORY METADATA ===') + lines.push(`Repository: ${data.metadata.repository}`) + lines.push(`Period: ${data.metadata.period}`) + + if (data.metadata.releaseInfo) { + lines.push( + `Release: ${data.metadata.releaseInfo.platform} ${data.metadata.releaseInfo.from} → ${data.metadata.releaseInfo.to}`, + ) + } + + lines.push(`Total commits: ${data.metadata.commitCount}`) + lines.push(`Pull requests: ${data.metadata.prCount}`) + + if (data.metadata.filtering) { + lines.push(`Filtered: ${data.metadata.filtering.trivialCommitsFiltered} trivial commits removed`) + } + lines.push('') + + // Stats section (compact) + lines.push('=== STATISTICS ===') + lines.push(`Authors: ${data.stats.totalAuthors}`) + lines.push(`Files changed: ${data.stats.filesChanged}`) + lines.push(`Lines: +${data.stats.linesAdded} -${data.stats.linesDeleted}`) + lines.push('') + + // Pull requests section with full bodies (up to prBodyLimit) + if (data.pullRequests.length > 0) { + lines.push('=== PULL REQUESTS ===') + for (const pr of data.pullRequests) { + lines.push(`PR #${pr.number}: ${pr.title} [${pr.state}] @${pr.author}`) + if (pr.body) { + // Include full PR body (already truncated to prBodyLimit during collection) + // Preserve markdown formatting with proper newlines + const bodyLines = pr.body.split('\n') + for (const line of bodyLines) { + lines.push(` ${line}`) + } + // Track token usage for PR bodies (rough approximation: 1 token ≈ 4 characters) + prBodyTokens += Math.ceil(pr.body.length / 4) + } + } + lines.push('') + } + + // Log PR body token contribution if significant + if (prBodyTokens > 0) { + this.logger.info(`PR bodies contribute ~${prBodyTokens.toLocaleString()} tokens to context`) + } + + // Commits section (ultra-compact format) + lines.push('=== COMMITS ===') + for (const commit of data.commits) { + // Format: sha | author | message | stats + const date = new Date(commit.timestamp).toISOString().split('T')[0] + lines.push(`${commit.sha.slice(0, 7)} | ${date} | ${commit.author.email.split('@')[0]} | ${commit.message}`) + + // If we have file information, show it compactly + if (commit.files && commit.files.length > 0) { + // Group files by directory for even more compact display + const fileGroups = new Map() + + for (const file of commit.files) { + const parts = file.path.split('/') + const dir = parts.length > 1 ? parts.slice(0, -1).join('/') : '.' + const filename = parts[parts.length - 1] + + if (!fileGroups.has(dir)) { + fileGroups.set(dir, []) + } + + // Status indicators: M=modified, A=added, D=deleted, R=renamed + const statusChar = file.status[0]?.toUpperCase() ?? 'M' + const changes = `${statusChar}:+${file.additions}-${file.deletions}` + const files = fileGroups.get(dir) + if (files) { + files.push(`${filename}(${changes})`) + } + } + + // Output grouped files + for (const [dir, files] of fileGroups) { + if (files.length <= 3) { + lines.push(` ${dir}/: ${files.join(' ')}`) + } else { + // If many files in same dir, summarize + lines.push(` ${dir}/: ${files.slice(0, 2).join(' ')} +${files.length - 2} more`) + } + } + + // Include diffs if available (already in compact diff format) + if (!skipDiffs) { + for (const file of commit.files) { + if (file.diff) { + lines.push(` --- ${file.path} ---`) + // Add indent to diff lines for readability + const diffLines = file.diff.split('\n').map((line: string) => ` ${line}`) + lines.push(...diffLines.slice(0, 10)) // Limit diff preview + if (diffLines.length > 10) { + lines.push(` ... (${diffLines.length - 10} more lines)`) + } + } + } + } + } + } + + return lines.join('\n') + } + + private async analyzeWithClaude(prompt: string): Promise> { + this.logger.info('Analyzing with Claude...') + + const model = this.config.model || 'claude-sonnet-4-5-20250929' + const stream = this.aiProvider.streamText({ + prompt, + model, + maxTokens: 64000, + temperature: 1, + }) + + // Stream and accumulate full response, emitting excerpts for UI + let fullText = '' + + // Buffers for accumulating deltas into meaningful chunks + let textBuffer = '' + let reasoningBuffer = '' + const MIN_EXCERPT_LENGTH = 150 // Minimum chars before emitting (higher threshold to reduce frequency) + const MAX_EXCERPT_LENGTH = 160 // Maximum chars per excerpt (very compact) + const MAX_BUFFER_LENGTH = 220 // Force emit if buffer gets too large + let lastEmittedTime = 0 + const MIN_EMIT_INTERVAL_MS = 2000 // Only emit excerpts every 2 seconds max + + for await (const chunk of stream) { + // Accumulate text + if (chunk.text) { + fullText += chunk.text + textBuffer += chunk.text + + // Check if we should emit a text excerpt + if (this.logger.emitStreamingExcerpt) { + const now = Date.now() + // Throttle excerpt emissions + if (now - lastEmittedTime >= MIN_EMIT_INTERVAL_MS) { + // Find sentence boundaries (including newlines for changelog-style content) + const lastSentenceEnd = Math.max( + textBuffer.lastIndexOf('.\n'), + textBuffer.lastIndexOf('.\n\n'), + textBuffer.lastIndexOf('. '), + textBuffer.lastIndexOf('!\n'), + textBuffer.lastIndexOf('?\n'), + textBuffer.lastIndexOf('\n\n'), // Double newline (paragraph break) + ) + + // Only emit if we have a good boundary and enough content + if ( + (lastSentenceEnd >= MIN_EXCERPT_LENGTH && lastSentenceEnd <= MAX_EXCERPT_LENGTH) || + (textBuffer.length >= MAX_BUFFER_LENGTH && lastSentenceEnd > MIN_EXCERPT_LENGTH) + ) { + const excerpt = textBuffer.slice(0, lastSentenceEnd > 0 ? lastSentenceEnd + 1 : MAX_EXCERPT_LENGTH).trim() + // Filter out markdown headers and very short content + if (excerpt.length >= MIN_EXCERPT_LENGTH && !excerpt.startsWith('##')) { + this.logger.emitStreamingExcerpt(excerpt, false) + textBuffer = textBuffer.slice(lastSentenceEnd > 0 ? lastSentenceEnd + 1 : MAX_EXCERPT_LENGTH) + lastEmittedTime = now + } + } + } + } else { + // CLI mode: write directly to console + process.stdout.write(chunk.text) + } + } + + // Accumulate reasoning + if (chunk.reasoning) { + reasoningBuffer += chunk.reasoning + + // Check if we should emit a reasoning excerpt (prefer reasoning over text) + if (this.logger.emitStreamingExcerpt) { + const now = Date.now() + // Throttle excerpt emissions + if (now - lastEmittedTime >= MIN_EMIT_INTERVAL_MS) { + // Find sentence boundaries for reasoning + const lastSentenceEnd = Math.max( + reasoningBuffer.lastIndexOf('.\n'), + reasoningBuffer.lastIndexOf('.\n\n'), + reasoningBuffer.lastIndexOf('. '), + reasoningBuffer.lastIndexOf('!\n'), + reasoningBuffer.lastIndexOf('?\n'), + reasoningBuffer.lastIndexOf('\n\n'), + ) + + // Only emit if we have a good boundary and enough content + if ( + (lastSentenceEnd >= MIN_EXCERPT_LENGTH && lastSentenceEnd <= MAX_EXCERPT_LENGTH) || + (reasoningBuffer.length >= MAX_BUFFER_LENGTH && lastSentenceEnd > MIN_EXCERPT_LENGTH) + ) { + const excerpt = reasoningBuffer + .slice(0, lastSentenceEnd > 0 ? lastSentenceEnd + 1 : MAX_EXCERPT_LENGTH) + .trim() + // Filter out markdown headers and very short content + if (excerpt.length >= MIN_EXCERPT_LENGTH && !excerpt.startsWith('##')) { + this.logger.emitStreamingExcerpt(excerpt, true) + reasoningBuffer = reasoningBuffer.slice(lastSentenceEnd > 0 ? lastSentenceEnd + 1 : MAX_EXCERPT_LENGTH) + lastEmittedTime = now + } + } + } + } + } + + if (chunk.isComplete) { + // Emit any remaining buffered content + if (this.logger.emitStreamingExcerpt) { + if (textBuffer.trim().length > 0) { + this.logger.emitStreamingExcerpt(textBuffer.trim(), false) + } + if (reasoningBuffer.trim().length > 0) { + this.logger.emitStreamingExcerpt(reasoningBuffer.trim(), true) + } + } else { + } + break + } + } + + // Try to parse as JSON if possible (especially for bug-bisect mode) + if (this.config.analysis.mode === 'bug-bisect') { + try { + // Extract JSON from markdown code blocks if present + const jsonMatch = fullText.match(/```(?:json)?\s*(\{[\s\S]*\})\s*```/) || fullText.match(/(\{[\s\S]*\})/) + const jsonText = jsonMatch && jsonMatch[1] ? jsonMatch[1] : fullText + const parsed = JSON.parse(jsonText) as Record + // Ensure it has the expected structure + if (parsed.suspiciousCommits && Array.isArray(parsed.suspiciousCommits)) { + return parsed + } + // If structure is wrong, wrap it + return { analysis: fullText, parsed } + } catch (error) { + this.logger.warn(`Failed to parse JSON response: ${error}`) + // Return as analysis text but try to extract any JSON-like content + return { analysis: fullText, error: 'Failed to parse JSON response' } + } + } + + // For other modes, try to parse as JSON but fallback to text + try { + return JSON.parse(fullText) as Record + } catch { + return { analysis: fullText } + } + } + + private async deliver(insights: Record, data: RepositoryData): Promise { + this.logger.info('Delivering results...') + + for (const output of this.config.outputs) { + await this.deliverToOutput({ insights, data, output }) + } + } + + private async deliverToOutput(args: { + insights: Record + data: RepositoryData + output: OutputConfig + }): Promise { + const { insights, data, output } = args + this.logger.info(`Delivering to ${output.type}...`) + + switch (output.type) { + case 'stdout': { + console.log('\n=== Analysis Results ===\n') + console.log(JSON.stringify(insights, null, 2)) + break + } + + case 'file': + case 'markdown': { + const path = output.target || 'analysis-output.md' + if (this.config.dryRun) { + this.logger.info(`[DRY RUN] Would save to file: ${path}`) + this.logger.info('[DRY RUN] Preview of content:') + console.log(this.formatAsMarkdown(insights, data).slice(0, 500) + '...') + } else { + // In production, this would use markdown-formatter subagent + await this.saveToFile({ insights, data, path }) + this.logger.info(`Saved to ${path}`) + } + break + } + + case 'slack': { + if (this.config.dryRun) { + this.logger.info(`[DRY RUN] Would publish to Slack channel: ${output.target}`) + this.logger.info(`[DRY RUN] Message preview: ${JSON.stringify(insights).slice(0, 200)}...`) + } else { + // In production, this would use slack-publisher subagent + this.logger.info(`Would publish to Slack channel: ${output.target}`) + this.logger.info('Note: Slack integration requires subagent implementation') + } + break + } + + default: + this.logger.warn(`Unknown output type: ${output.type}`) + } + } + + private async saveToFile(args: { + insights: Record + data: RepositoryData + path: string + }): Promise { + const { insights, data, path } = args + const content = this.formatAsMarkdown(insights, data) + await Bun.write(path, content) + + // Also save to analysis folder + await this.analysisWriter.saveReport(content) + } + + private formatAsMarkdown(insights: Record, data: RepositoryData): string { + const lines: string[] = [] + + lines.push(`# Repository Analysis: ${data.metadata.repository}`) + lines.push(`*Period: ${data.metadata.period}*`) + lines.push(`*Generated: ${data.metadata.collectedAt}*`) + lines.push('') + + if (insights.themes && Array.isArray(insights.themes)) { + lines.push('## Themes') + for (const theme of insights.themes) { + if (theme && typeof theme === 'object' && 'title' in theme && 'description' in theme) { + lines.push(`### ${String(theme.title)}`) + lines.push(String(theme.description)) + lines.push('') + } + } + } + + if (insights.highlights && Array.isArray(insights.highlights)) { + lines.push('## Highlights') + for (const highlight of insights.highlights) { + lines.push(`- ${String(highlight)}`) + } + lines.push('') + } + + if (insights.metrics && typeof insights.metrics === 'object') { + const metrics = insights.metrics as Record + lines.push('## Metrics') + if (typeof metrics.total_commits === 'number') { + lines.push(`- Commits: ${metrics.total_commits}`) + } + if (typeof metrics.total_prs === 'number') { + lines.push(`- Pull Requests: ${metrics.total_prs}`) + } + if (typeof metrics.active_contributors === 'number') { + lines.push(`- Contributors: ${metrics.active_contributors}`) + } + lines.push('') + } + + if (typeof insights === 'string') { + lines.push('## Analysis') + lines.push(insights) + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + } else if (insights && typeof insights === 'object' && 'analysis' in insights && insights.analysis) { + lines.push('## Analysis') + lines.push(String(insights.analysis)) + } + + return lines.join('\n') + } + + private async generateSummary(data: RepositoryData): Promise { + if (!this.config.saveArtifacts) { + return + } + + // Get stats from data collector if available + const totalCommits = data.metadata.filtering?.totalCommitsFound ?? data.metadata.commitCount + const trivialFiltered = data.metadata.filtering?.trivialCommitsFiltered ?? 0 + + await this.analysisWriter.saveSummary({ + config: this.config, + dataCollection: { + totalCommits, + trivialCommitsFiltered: trivialFiltered, + commitsAnalyzed: data.commits.length, + prsExtracted: data.pullRequests.length, + tokensEstimated: Math.round((data.commits.length * 100 + data.pullRequests.length * 50) / 4), + }, + timing: { + dataCollection: this.timings.dataCollection, + claudeAnalysis: this.timings.claudeAnalysis, + total: Date.now() - this.startTime, + }, + }) + } +} diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts new file mode 100644 index 00000000000..d6a57aae330 --- /dev/null +++ b/apps/cli/src/index.ts @@ -0,0 +1,2 @@ +// eslint-disable-next-line @typescript-eslint/triple-slash-reference +/// diff --git a/apps/cli/src/lib/ai-provider-vercel.ts b/apps/cli/src/lib/ai-provider-vercel.ts new file mode 100644 index 00000000000..d4aeb3d148a --- /dev/null +++ b/apps/cli/src/lib/ai-provider-vercel.ts @@ -0,0 +1,143 @@ +import { AnthropicProviderOptions, anthropic } from '@ai-sdk/anthropic' +import type { AIProvider, GenerateTextInput, StreamChunk, StreamTextInput } from '@universe/cli/src/lib/ai-provider' +import { generateText, streamText } from 'ai' + +/** + * Vercel AI SDK implementation of AIProvider + * + * Maps Vercel AI SDK responses to our contract interface. + */ +export class VercelAIProvider implements AIProvider { + constructor(private apiKey?: string) { + // API key can be provided via constructor or ANTHROPIC_API_KEY env var + // If provided, set it as environment variable for Vercel AI SDK to use + if (apiKey) { + process.env.ANTHROPIC_API_KEY = apiKey + } + } + + private processFullStreamChunk( + chunk: unknown, + accumulators: { fullText: { value: string }; fullReasoning: { value: string } }, + ): StreamChunk | null { + const chunkObj = typeof chunk === 'object' && chunk !== null && 'type' in chunk ? chunk : null + + if (!chunkObj) { + // Fallback: treat as text chunk + const textChunk = String(chunk) + accumulators.fullText.value += textChunk + return { + text: textChunk, + isComplete: false, + } + } + + const chunkType = chunkObj.type as string + + // Reasoning delta chunks (per Vercel AI SDK docs) + if (chunkType === 'reasoning-delta') { + const reasoningContent = String((chunkObj as { text?: string }).text || '') + if (reasoningContent) { + accumulators.fullReasoning.value += reasoningContent + return { + text: '', + reasoning: reasoningContent, + isComplete: false, + } + } + return null + } + + // Text delta chunks (per Vercel AI SDK docs) + if (chunkType === 'text-delta') { + const textContent = String((chunkObj as { text?: string }).text || '') + if (textContent) { + accumulators.fullText.value += textContent + return { + text: textContent, + reasoning: undefined, + isComplete: false, + } + } + } + + return null + } + + async *streamText(input: StreamTextInput): AsyncGenerator { + const model = anthropic(input.model) + + const result = streamText({ + model, + prompt: input.prompt, + system: input.systemPrompt, + temperature: input.temperature, + ...(input.maxTokens && { maxTokens: input.maxTokens }), + providerOptions: { + anthropic: { + thinking: { type: 'enabled', budgetTokens: 63999 }, + sendReasoning: true, + } satisfies AnthropicProviderOptions, + }, + }) + + const accumulators = { + fullText: { value: '' }, + fullReasoning: { value: '' }, + } + + // Check if fullStream is available (contains reasoning chunks when sendReasoning is true) + // Process fullStream if available to access reasoning, otherwise use textStream + const textStream = result.textStream + const fullStream = 'fullStream' in result ? (result as { fullStream?: AsyncIterable }).fullStream : null + + if (fullStream) { + for await (const chunk of fullStream) { + const processedChunk = this.processFullStreamChunk(chunk, accumulators) + if (processedChunk) { + yield processedChunk + } + } + } else { + // Fallback: process text stream normally + for await (const chunk of textStream) { + const textChunk = String(chunk) + accumulators.fullText.value += textChunk + yield { + text: textChunk, + isComplete: false, + } + } + } + + // Yield final complete chunk (signal only, no duplicate content) + // The orchestrator already accumulated all delta chunks during streaming, + // so we only need to signal completion without re-sending the entire text + yield { + text: '', + reasoning: undefined, + isComplete: true, + } + } + + async generateText(input: GenerateTextInput): Promise { + const model = anthropic(input.model) + + const result = await generateText({ + model, + prompt: input.prompt, + system: input.systemPrompt, + temperature: input.temperature, + ...(input.maxTokens && { maxTokens: input.maxTokens }), + }) + + return result.text + } +} + +/** + * Factory function to create a VercelAIProvider instance + */ +export function createVercelAIProvider(apiKey?: string): VercelAIProvider { + return new VercelAIProvider(apiKey || process.env.ANTHROPIC_API_KEY) +} diff --git a/apps/cli/src/lib/ai-provider.ts b/apps/cli/src/lib/ai-provider.ts new file mode 100644 index 00000000000..24c37fbd150 --- /dev/null +++ b/apps/cli/src/lib/ai-provider.ts @@ -0,0 +1,50 @@ +/** + * AI Provider Contract + * + * Defines the interface for AI text generation providers. + * Allows swapping implementations (e.g., Claude SDK, Vercel AI SDK, etc.) + */ + +export interface StreamTextInput { + prompt: string + systemPrompt?: string + model: string + temperature?: number + maxTokens?: number +} + +export interface StreamChunk { + text: string + isComplete: boolean + reasoning?: string +} + +export interface GenerateTextInput { + prompt: string + systemPrompt?: string + model: string + temperature?: number + maxTokens?: number +} + +/** + * AI Provider interface contract + * + * Provides methods for streaming and non-streaming text generation. + * Implementations should map to their respective SDKs while maintaining this interface. + */ +export interface AIProvider { + /** + * Stream text generation with incremental chunks + * @param input - Configuration for text generation + * @returns Async generator yielding text chunks + */ + streamText(input: StreamTextInput): AsyncGenerator + + /** + * Generate complete text without streaming + * @param input - Configuration for text generation + * @returns Complete generated text + */ + generateText(input: GenerateTextInput): Promise +} diff --git a/apps/cli/src/lib/analysis-writer.ts b/apps/cli/src/lib/analysis-writer.ts new file mode 100644 index 00000000000..e62a3606af9 --- /dev/null +++ b/apps/cli/src/lib/analysis-writer.ts @@ -0,0 +1,241 @@ +import { mkdir } from 'node:fs/promises' +import { join } from 'node:path' + +/** + * Utility for writing analysis artifacts to disk for debugging and audit + */ +export class AnalysisWriter { + private runId: string + private basePath: string + private runPath: string + + constructor(basePath: string = '.analysis') { + this.basePath = basePath + this.runId = this.generateRunId() + this.runPath = join(this.basePath, this.runId) + } + + /** + * Generate a unique run ID based on timestamp + */ + private generateRunId(): string { + const now = new Date() + const year = now.getFullYear() + const month = String(now.getMonth() + 1).padStart(2, '0') + const day = String(now.getDate()).padStart(2, '0') + const hours = String(now.getHours()).padStart(2, '0') + const minutes = String(now.getMinutes()).padStart(2, '0') + const seconds = String(now.getSeconds()).padStart(2, '0') + + return `analysis-${year}${month}${day}-${hours}${minutes}${seconds}` + } + + /** + * Get the run ID for this analysis + */ + getRunId(): string { + return this.runId + } + + /** + * Get the full path to the run directory + */ + getRunPath(): string { + return this.runPath + } + + /** + * Initialize the run directory + */ + async initialize(): Promise { + await mkdir(this.runPath, { recursive: true }) + } + + /** + * Save JSON data to a file + */ + async saveJson(filename: string, data: unknown): Promise { + const filepath = join(this.runPath, filename) + await Bun.write(filepath, JSON.stringify(data, null, 2)) + } + + /** + * Save text content to a file + */ + async saveText(filename: string, content: string): Promise { + const filepath = join(this.runPath, filename) + await Bun.write(filepath, content) + } + + /** + * Save the configuration used for this run + */ + async saveConfig(config: unknown): Promise { + await this.saveJson('config.json', config) + } + + /** + * Save commit data + */ + async saveCommits(commits: unknown[], metadata?: unknown): Promise { + await this.saveJson('commits.json', { + count: commits.length, + metadata, + commits, + }) + } + + /** + * Save pull request data + */ + async savePullRequests(prs: unknown[]): Promise { + await this.saveJson('pull-requests.json', { + count: prs.length, + pullRequests: prs, + }) + } + + /** + * Save repository statistics + */ + async saveStats(stats: unknown): Promise { + await this.saveJson('stats.json', stats) + } + + /** + * Save the context sent to Claude + */ + async saveContext(context: string): Promise { + await this.saveText('context.json', context) + } + + /** + * Save the prompt used + */ + async savePrompt(prompt: string): Promise { + await this.saveText('prompt.md', prompt) + } + + /** + * Save the complete input to Claude + */ + async saveClaudeInput(input: string): Promise { + await this.saveText('claude-input.md', input) + } + + /** + * Save Claude's response + */ + async saveClaudeOutput(output: unknown): Promise { + if (typeof output === 'string') { + await this.saveText('claude-output.md', output) + } else { + await this.saveJson('claude-output.json', output) + } + } + + /** + * Save the final report + */ + async saveReport(report: string): Promise { + await this.saveText('report.md', report) + } + + /** + * Save a debug summary + */ + async saveSummary(data: { + config: unknown + dataCollection: { + totalCommits: number + trivialCommitsFiltered: number + commitsAnalyzed: number + prsExtracted: number + tokensEstimated?: number + } + filesFiltered?: { + snapshots: number + lockfiles: number + generated: number + other: number + } + timing?: { + dataCollection: number + claudeAnalysis: number + total: number + } + }): Promise { + const { config, dataCollection, filesFiltered, timing } = data + + // Type assertion for config structure + const typedConfig = config as { + analysis?: { + mode?: string + releaseOptions?: { + platform?: string + version?: string + compareWith?: string + } + } + collect?: { + since?: string + } + } + + const summary = `# Analysis Run: ${this.runId} + +## Configuration +- Mode: ${typedConfig.analysis?.mode || 'unknown'} +${ + typedConfig.analysis?.releaseOptions + ? `- Platform: ${typedConfig.analysis.releaseOptions.platform} +- Version: ${typedConfig.analysis.releaseOptions.version} +- Comparing with: ${typedConfig.analysis.releaseOptions.compareWith || 'previous'}` + : '' +} +- Since: ${typedConfig.collect?.since || 'unknown'} + +## Data Collection +- Total commits found: ${dataCollection.totalCommits} +- Trivial commits filtered: ${dataCollection.trivialCommitsFiltered} +- Commits analyzed: ${dataCollection.commitsAnalyzed} +- PRs extracted: ${dataCollection.prsExtracted} +${dataCollection.tokensEstimated ? `- Tokens estimated: ~${dataCollection.tokensEstimated.toLocaleString()}` : ''} + +${ + filesFiltered + ? `## Files Filtered +- Snapshots: ${filesFiltered.snapshots} files +- Lockfiles: ${filesFiltered.lockfiles} files +- Generated: ${filesFiltered.generated} files +- Other: ${filesFiltered.other} files` + : '' +} + +${ + timing + ? `## Timing +- Data collection: ${(timing.dataCollection / 1000).toFixed(1)}s +- Claude analysis: ${(timing.claudeAnalysis / 1000).toFixed(1)}s +- Total: ${(timing.total / 1000).toFixed(1)}s` + : '' +} + +## Output Location +- Run ID: ${this.runId} +- Path: ${this.runPath}/ +` + + await this.saveText('summary.md', summary) + } + + /** + * Save list of filtered files + */ + async saveFilteredFiles(files: { path: string; reason: string }[]): Promise { + await this.saveJson('filtered-files.json', { + count: files.length, + files, + }) + } +} diff --git a/apps/cli/src/lib/cache-keys.ts b/apps/cli/src/lib/cache-keys.ts new file mode 100644 index 00000000000..dffacb069c7 --- /dev/null +++ b/apps/cli/src/lib/cache-keys.ts @@ -0,0 +1,106 @@ +import { createHash } from 'node:crypto' +import { type CollectOptions } from '@universe/cli/src/core/data-collector' + +/** + * Generate deterministic cache keys from CollectOptions + * Excludes non-deterministic fields like commitDataConfig that affect output format + */ + +/** + * Create a hash from a string + */ +function hash(input: string): string { + return createHash('sha256').update(input).digest('hex').slice(0, 16) +} + +/** + * Generate cache key components from CollectOptions (excluding output formatting options) + */ +function getCacheKeyComponents(options: CollectOptions): string { + const parts: string[] = [] + + // Repository identifier + if (options.repository?.owner && options.repository.name) { + parts.push(`repo:${options.repository.owner}/${options.repository.name}`) + } else if (options.repoPath) { + parts.push(`repopath:${options.repoPath}`) + } + + // Time-based query + parts.push(`since:${options.since}`) + + // Branch filter + if (options.branch) { + parts.push(`branch:${options.branch}`) + } + + // Author filter + if (options.author) { + parts.push(`author:${options.author}`) + } + + // Team filters + if (options.teamFilter?.length) { + const sortedEmails = [...options.teamFilter].sort().join(',') + parts.push(`team:${sortedEmails}`) + } + + if (options.teamUsernames?.length) { + const sortedUsernames = [...options.teamUsernames].sort().join(',') + parts.push(`usernames:${sortedUsernames}`) + } + + // Include open PRs flag + if (options.includeOpenPrs) { + parts.push('includeOpenPrs:true') + } + + // Release comparison (deterministic by version range) + if (options.releaseComparison) { + parts.push(`release:${options.releaseComparison.from.version}-${options.releaseComparison.to.version}`) + parts.push(`platform:${options.releaseComparison.to.platform}`) + parts.push(`range:${options.releaseComparison.commitRange}`) + } + + // Exclude trivial commits flag + if (options.excludeTrivialCommits) { + parts.push('excludeTrivial:true') + } + + return parts.join('|') +} + +/** + * Generate cache key for commits + */ +export function getCommitsCacheKey(options: CollectOptions): string { + const components = getCacheKeyComponents(options) + return `commits:${hash(components)}` +} + +/** + * Generate cache key for pull requests + */ +export function getPullRequestsCacheKey(options: CollectOptions): string { + const components = getCacheKeyComponents(options) + return `prs:${hash(components)}` +} + +/** + * Generate cache key for stats + */ +export function getStatsCacheKey(options: CollectOptions): string { + const components = getCacheKeyComponents(options) + return `stats:${hash(components)}` +} + +/** + * Generate pattern to invalidate all cache entries for a repository + */ +export function getRepositoryCachePattern(repository?: { owner?: string; name?: string }): string { + if (repository?.owner && repository.name) { + const repoHash = hash(`repo:${repository.owner}/${repository.name}`) + return `%:${repoHash}%` + } + return '%' +} diff --git a/apps/cli/src/lib/cache-provider-sqlite.ts b/apps/cli/src/lib/cache-provider-sqlite.ts new file mode 100644 index 00000000000..4b7e2119fd1 --- /dev/null +++ b/apps/cli/src/lib/cache-provider-sqlite.ts @@ -0,0 +1,130 @@ +import { Database } from 'bun:sqlite' +import { existsSync } from 'node:fs' +import { mkdir } from 'node:fs/promises' +import { join } from 'node:path' +import { type CacheProvider } from '@universe/cli/src/lib/cache-provider' + +/** + * SQLite implementation of CacheProvider using Bun's built-in SQLite + */ +export class SqliteCacheProvider implements CacheProvider { + private db: Database + private readonly dbPath: string + + constructor(dbPath?: string) { + // Default to ~/.gh-agent/cache.db + this.dbPath = dbPath || join(process.env.HOME || process.env.USERPROFILE || '.', '.gh-agent', 'cache.db') + this.ensureCacheDirectorySync() + this.db = new Database(this.dbPath) + this.initializeSchema() + this.cleanupExpired() + } + + private ensureCacheDirectorySync(): void { + const dir = join(this.dbPath, '..') + if (!existsSync(dir)) { + try { + // Use sync version for constructor - Bun will create parent directories + mkdir(dir, { recursive: true }).catch((error) => { + // eslint-disable-next-line no-console + console.error(`[WARN] Failed to create cache directory: ${error}`) + }) + } catch { + // Ignore - will fail gracefully on database creation + } + } + } + + private initializeSchema(): void { + this.db.exec(` + CREATE TABLE IF NOT EXISTS cache_entries ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + expires_at INTEGER, + created_at INTEGER NOT NULL + ); + + CREATE INDEX IF NOT EXISTS idx_expires_at ON cache_entries(expires_at); + `) + } + + /** + * Remove expired entries (called on initialization and periodically) + */ + private cleanupExpired(): void { + const now = Date.now() + this.db.exec(`DELETE FROM cache_entries WHERE expires_at IS NOT NULL AND expires_at < ${now}`) + } + + async get(key: string): Promise { + try { + const now = Date.now() + const stmt = this.db.prepare(` + SELECT value, expires_at + FROM cache_entries + WHERE key = ? AND (expires_at IS NULL OR expires_at >= ?) + `) + + const result = stmt.get(key, now) as { value: string; expires_at: number | null } | undefined + + if (!result) { + return null + } + + // Clean up expired entries periodically (every 100 reads) + if (Math.random() < 0.01) { + this.cleanupExpired() + } + + return JSON.parse(result.value) as T + } catch (_error) { + return null + } + } + + // eslint-disable-next-line max-params -- Required to match CacheProvider interface + async set(key: string, value: T, ttlSeconds?: number): Promise { + try { + const serialized = JSON.stringify(value) + const now = Date.now() + const expiresAt = ttlSeconds ? now + ttlSeconds * 1000 : null + + const stmt = this.db.prepare(` + INSERT OR REPLACE INTO cache_entries (key, value, expires_at, created_at) + VALUES (?, ?, ?, ?) + `) + + stmt.run(key, serialized, expiresAt, now) + } catch (_error) { + // Don't throw - graceful degradation + } + } + + async invalidate(key: string): Promise { + try { + const stmt = this.db.prepare('DELETE FROM cache_entries WHERE key = ?') + stmt.run(key) + } catch (_error) {} + } + + async invalidatePattern(pattern: string): Promise { + try { + // SQLite uses LIKE for pattern matching + const stmt = this.db.prepare('DELETE FROM cache_entries WHERE key LIKE ?') + stmt.run(pattern) + } catch (_error) {} + } + + async clear(): Promise { + try { + this.db.exec('DELETE FROM cache_entries') + } catch (_error) {} + } + + /** + * Close the database connection (call when done) + */ + close(): void { + this.db.close() + } +} diff --git a/apps/cli/src/lib/cache-provider.ts b/apps/cli/src/lib/cache-provider.ts new file mode 100644 index 00000000000..e6126843860 --- /dev/null +++ b/apps/cli/src/lib/cache-provider.ts @@ -0,0 +1,41 @@ +// ============================================================================ +// Cache Provider Interface +// ============================================================================ + +/** + * Contract for cache providers - allows swapping implementations + * (e.g., SQLite, Redis, in-memory) without changing consuming code + */ +export interface CacheProvider { + /** + * Retrieve a value from cache by key + * @param key Cache key + * @returns Cached value or null if not found/expired + */ + get(key: string): Promise + + /** + * Store a value in cache with optional TTL + * @param key Cache key + * @param value Value to cache (will be serialized as JSON) + * @param ttlSeconds Optional time-to-live in seconds (default: no expiration) + */ + set(key: string, value: T, ttlSeconds?: number): Promise + + /** + * Remove a specific key from cache + * @param key Cache key to invalidate + */ + invalidate(key: string): Promise + + /** + * Remove all keys matching a pattern (supports SQL LIKE patterns) + * @param pattern Pattern to match (e.g., "commits:%" or "prs:abc%") + */ + invalidatePattern(pattern: string): Promise + + /** + * Clear all entries from cache + */ + clear(): Promise +} diff --git a/apps/cli/src/lib/logger.ts b/apps/cli/src/lib/logger.ts new file mode 100644 index 00000000000..5608423ca3d --- /dev/null +++ b/apps/cli/src/lib/logger.ts @@ -0,0 +1,221 @@ +/** + * Logger interface for dependency injection + * Allows different logging strategies for interactive vs non-interactive modes + */ +export interface Logger { + info(message: string): void + warn(message: string): void + error(message: string): void + debug(message: string): void + /** + * Emit a streaming excerpt from agent thinking/output (optional, for UI progress updates) + */ + emitStreamingExcerpt?(excerpt: string, isReasoning?: boolean): void +} + +/** + * Progress stage type for UI progress events + */ +export type ProgressStage = 'idle' | 'collecting' | 'analyzing' | 'delivering' | 'complete' | 'error' + +/** + * Progress event type for categorizing messages + */ +export type ProgressEventType = 'reasoning' | 'output' | 'info' + +/** + * Progress event interface for UI updates + */ +export interface ProgressEvent { + stage: ProgressStage + message?: string + progress?: number // 0-100 + cacheInfo?: { + type: 'commits' | 'prs' | 'stats' + count: number + } + /** + * Whether this is reasoning (thinking) content from AI + */ + isReasoning?: boolean + /** + * Type of event for visual distinction in UI + */ + eventType?: ProgressEventType +} + +/** + * ConsoleLogger - Direct console output for non-interactive CLI mode + */ +export class ConsoleLogger implements Logger { + constructor(private readonly verbose: boolean = false) {} + + info(message: string): void { + // eslint-disable-next-line no-console + console.log(`[INFO] ${message}`) + } + + warn(message: string): void { + // eslint-disable-next-line no-console + console.warn(`[WARN] ${message}`) + } + + error(message: string): void { + // eslint-disable-next-line no-console + console.error(`[ERROR] ${message}`) + } + + debug(message: string): void { + if (this.verbose) { + // eslint-disable-next-line no-console + console.log(`[DEBUG] ${message}`) + } + } +} + +/** + * ProgressLogger - Emits progress events for interactive Ink UI mode + * Suppresses stdout to avoid interfering with Ink rendering + */ +export class ProgressLogger implements Logger { + constructor( + private readonly onProgress: (event: ProgressEvent) => void, + private readonly verbose: boolean = false, + ) {} + + info(message: string): void { + // Suppress redundant messages that are already handled by stage transitions + if (this.shouldSuppress(message)) { + return + } + + // Parse message to determine stage and emit appropriate progress event + const stage = this.determineStage(message) + const cleanMessage = message.replace(/^\[INFO\]\s*/, '').trim() + + // Detect cache hit messages and extract cache info + const cacheInfo = this.parseCacheInfo(message) + + if (cleanMessage) { + this.onProgress({ + stage, + message: cleanMessage, + eventType: 'info', + ...(cacheInfo && { cacheInfo }), + }) + } + } + + warn(message: string): void { + const cleanMessage = message.replace(/^\[WARN\]\s*/, '').trim() + // Warnings don't change stage, but emit as progress updates + if (cleanMessage) { + this.onProgress({ stage: 'collecting', message: cleanMessage, eventType: 'info' }) + } + } + + error(message: string): void { + const cleanMessage = message.replace(/^\[ERROR\]\s*/, '').trim() + this.onProgress({ stage: 'error', message: cleanMessage, eventType: 'info' }) + } + + debug(message: string): void { + if (this.verbose) { + const cleanMessage = message.replace(/^\[DEBUG\]\s*/, '').trim() + // Debug messages are emitted as progress updates during current stage + if (cleanMessage) { + this.onProgress({ stage: 'collecting', message: cleanMessage, eventType: 'info' }) + } + } + } + + emitStreamingExcerpt(excerpt: string, isReasoning = false): void { + // Excerpts are already trimmed to meaningful chunks (complete sentences or size limits) + // Just ensure they're not too long for display (safety check) + const maxDisplayLength = 250 + const displayExcerpt = excerpt.length > maxDisplayLength ? `${excerpt.slice(0, maxDisplayLength)}...` : excerpt + + // Emit as progress event during analyzing stage with reasoning flag and event type + this.onProgress({ + stage: 'analyzing', + message: displayExcerpt, + isReasoning, + eventType: isReasoning ? 'reasoning' : 'output', + }) + } + + /** + * Check if a message should be suppressed (not emitted as progress event) + */ + private shouldSuppress(message: string): boolean { + return message.includes('Scanning for') || message.includes('Starting repository analysis') + } + + /** + * Determine the progress stage based on message content + */ + private determineStage(message: string): ProgressEvent['stage'] { + // Stage transitions - check these first + if (message.includes('Collecting repository data')) { + return 'collecting' + } + if (message.includes('Running analysis') || message.includes('Analyzing with Claude')) { + return 'analyzing' + } + if (message.includes('Delivering to') || message.includes('Delivering results')) { + return 'delivering' + } + if (message.includes('Analysis complete')) { + return 'complete' + } + + // Batch progress updates + if ( + (message.includes('Fetched') && message.includes('PRs') && message.includes('batch')) || + message.includes('Successfully fetched') || + (message.includes('Found') && (message.includes('commits') || message.includes('pull requests'))) || + message.includes('Extracting PR information') || + message.includes('Getting commits for release') + ) { + return 'collecting' + } + + // Default to collecting stage for other INFO messages + return 'collecting' + } + + /** + * Parse cache hit information from log messages + */ + private parseCacheInfo(message: string): ProgressEvent['cacheInfo'] | undefined { + // Pattern: "Cache hit: Found X commits in cache" + // Pattern: "Cache hit: Found X PRs in cache" + const cacheHitMatch = message.match(/Cache hit: Found (\d+) (commits|PRs|pull requests) in cache/i) + if (cacheHitMatch) { + const count = parseInt(cacheHitMatch[1] || '0', 10) + const typeStr = cacheHitMatch[2]?.toLowerCase() || '' + + let type: 'commits' | 'prs' | 'stats' + if (typeStr.includes('commit')) { + type = 'commits' + } else if (typeStr.includes('pr') || typeStr.includes('pull request')) { + type = 'prs' + } else { + return undefined + } + + return { type, count } + } + + // Pattern: "Cache hit: Found X stats in cache" (if stats caching exists) + const statsCacheMatch = message.match(/Cache hit: Found (\d+) stats? in cache/i) + if (statsCacheMatch) { + return { + type: 'stats', + count: parseInt(statsCacheMatch[1] || '0', 10), + } + } + + return undefined + } +} diff --git a/apps/cli/src/lib/pr-body-cleaner.ts b/apps/cli/src/lib/pr-body-cleaner.ts new file mode 100644 index 00000000000..d759f98ae0a --- /dev/null +++ b/apps/cli/src/lib/pr-body-cleaner.ts @@ -0,0 +1,496 @@ +/** + * PR Body Cleaner + * + * Intelligently removes unnecessary content from PR bodies while preserving + * important technical details, code blocks, and CURSOR_SUMMARY blocks. + */ + +/** + * Cleans a PR body by removing unnecessary content while preserving valuable information + * @param body The raw PR body text + * @returns Cleaned PR body with unnecessary content removed + */ +export function cleanPRBody(body: string): string { + if (!body || body.trim().length === 0) { + return body + } + + let cleaned = body + + // Step 1: Extract CURSOR_SUMMARY content and remove all HTML comment markers + cleaned = removeHTMLCommentsExceptCursorSummary(cleaned) + + // Step 1.5: Remove Cursor Bugbot footer notes (may appear outside CURSOR_SUMMARY blocks) + cleaned = removeCursorBugbotFooters(cleaned) + + // Step 2: Remove image/video markdown + cleaned = removeImageVideoMarkdown(cleaned) + + // Step 3: Clean tables (remove if only images/videos) + cleaned = cleanTables(cleaned) + + // Step 4: Clean external links (keep text, remove long URLs) + cleaned = cleanExternalLinks(cleaned) + + // Step 5: Remove empty sections + cleaned = removeEmptySections(cleaned) + + // Step 6: Remove minimal value sections + cleaned = removeMinimalValueSections(cleaned) + + // Step 7: Remove redundant sections (screen captures, testing) + cleaned = removeRedundantSections(cleaned) + + // Step 8: Remove redundant headers + cleaned = removeRedundantHeaders(cleaned) + + // Step 9: Aggressive whitespace normalization (max 1 blank line) + cleaned = normalizeWhitespace(cleaned) + + return cleaned +} + +/** + * Extract CURSOR_SUMMARY content and remove all HTML comment markers + */ +function removeHTMLCommentsExceptCursorSummary(text: string): string { + // Extract CURSOR_SUMMARY content (without the comment markers) + const cursorSummaryPlaceholder = '___CURSOR_SUMMARY_PLACEHOLDER___' + const cursorSummaryRegex = /([\s\S]*?)/gi + const summaries: string[] = [] + let matchIndex = 0 + + // Extract just the content between the markers + let textWithProtection = text.replace(cursorSummaryRegex, (match, content) => { + // Clean the content before storing: remove Cursor Bugbot footer notes + let cleanedContent = content.trim() + + // Remove Cursor Bugbot footer notes (metadata lines) + cleanedContent = cleanedContent.replace( + />\s*\[!NOTE\]\s*\n\s*>\s*\[Cursor Bugbot\].*?Configure \[here\].*?<\/sup>/gi, + '', + ) + cleanedContent = cleanedContent.replace( + />\s*Written by \[Cursor Bugbot\].*?Configure \[here\].*?<\/sup>/gi, + '', + ) + cleanedContent = cleanedContent.replace( + />\s*\[Cursor Bugbot\].*?is generating a summary.*?Configure \[here\].*?<\/sup>/gi, + '', + ) + + // Clean up any leftover "> " prefixes from blockquotes + cleanedContent = cleanedContent.replace(/^>\s*/gm, '') + + summaries.push(cleanedContent.trim()) // Store cleaned content + return `${cursorSummaryPlaceholder}${matchIndex++}` + }) + + // Remove all other HTML comments + let previousTextWithProtection: string + do { + previousTextWithProtection = textWithProtection + textWithProtection = textWithProtection.replace(//g, '') + } while (textWithProtection !== previousTextWithProtection) + + // Restore CURSOR_SUMMARY content (without comment markers and footers) + summaries.forEach((summary, index) => { + textWithProtection = textWithProtection.replace(`${cursorSummaryPlaceholder}${index}`, summary) + }) + + return textWithProtection +} + +/** + * Remove Cursor Bugbot footer notes (metadata lines) + */ +function removeCursorBugbotFooters(text: string): string { + let cleaned = text + + // Remove standalone footer notes (outside CURSOR_SUMMARY blocks) + // Pattern 1: > [!NOTE]\n> [Cursor Bugbot]...Configure [here]... + cleaned = cleaned.replace( + />\s*\[!NOTE\]\s*\n\s*>\s*\[Cursor Bugbot\][\s\S]*?Configure \[here\][\s\S]*?<\/sup>\s*/gi, + '', + ) + + // Pattern 2: > Written by [Cursor Bugbot]...Configure [here]... + cleaned = cleaned.replace(/>\s*Written by \[Cursor Bugbot\][\s\S]*?Configure \[here\][\s\S]*?<\/sup>\s*/gi, '') + + // Pattern 3: > [Cursor Bugbot]...is generating a summary...Configure [here]... + cleaned = cleaned.replace( + />\s*\[Cursor Bugbot\][\s\S]*?is generating a summary[\s\S]*?Configure \[here\][\s\S]*?<\/sup>\s*/gi, + '', + ) + + // Pattern 4: Standalone note blocks with Cursor Bugbot (any variant) + cleaned = cleaned.replace(/>\s*\[!NOTE\]\s*\n\s*>\s*\[Cursor Bugbot\][\s\S]*?<\/sup>\s*/gi, '') + + // Pattern 5: Any blockquote line containing Cursor Bugbot footer + cleaned = cleaned.replace(/>\s*\[Cursor Bugbot\][\s\S]*?<\/sup>\s*/gi, '') + + return cleaned +} + +/** + * Remove image and video markdown links + */ +function removeImageVideoMarkdown(text: string): string { + // Remove image markdown: ![alt](url) or ![alt](url "title") + let cleaned = text.replace(/!\[([^\]]*)\]\([^)]+\)/g, '') + + // Remove video markdown patterns like [Screen Recording ...](url) + cleaned = cleaned.replace(/\[Screen Recording[^\]]*\]\([^)]+\)/gi, '') + cleaned = cleaned.replace(/\[Screen Recording[^\]]*\]\([^)]+\)/gi, '') + + // Remove tags + cleaned = cleaned.replace(/]*>/gi, '') + + // Remove video links that look like markdown + cleaned = cleaned.replace(/\[.*\.mov.*\]\([^)]+\)/gi, '') + cleaned = cleaned.replace(/\[.*\.mp4.*\]\([^)]+\)/gi, '') + + return cleaned +} + +/** + * Remove tables that only contain images/videos + */ +function cleanTables(text: string): string { + // Match table blocks - split into parts to avoid unsafe regex + const lines = text.split('\n') + const result: string[] = [] + let inTable = false + let tableLines: string[] = [] + + for (let i = 0; i < lines.length; i++) { + const line = lines[i] + if (!line) { + result.push('') + continue + } + + const isTableRow = line.trim().startsWith('|') && line.trim().endsWith('|') + + if (isTableRow) { + if (!inTable) { + inTable = true + tableLines = [] + } + tableLines.push(line) + } else { + if (inTable) { + // Process accumulated table + const tableMatch = tableLines.join('\n') + if (shouldRemoveTable(tableMatch)) { + // Skip this table + } else { + result.push(...tableLines) + } + inTable = false + tableLines = [] + } + result.push(line) + } + } + + // Handle table at end of text + if (inTable && tableLines.length > 0) { + const tableMatch = tableLines.join('\n') + if (!shouldRemoveTable(tableMatch)) { + result.push(...tableLines) + } + } + + return result.join('\n') +} + +function shouldRemoveTable(tableMatch: string): boolean { + // Remove empty tables (only separators like | --- | --- |) + const cleanedForEmpty = tableMatch.replace(/[\s|:-]/g, '') + if (cleanedForEmpty.length === 0) { + return true + } + + // Check if table only contains image/video links or empty cells + const imageVideoPattern = /!\[|\]\([^)]+\)|Screen Recording|\.mov|\.mp4|\.png|\.jpg|\.jpeg|\.gif|\.webp/gi + const hasOnlyImagesOrVideos = imageVideoPattern.test(tableMatch) + const textPattern = /[a-zA-Z]{3,}/ + const cleanedTable = tableMatch.replace(/!\[|\]\([^)]+\)|Screen Recording/gi, '') + const hasTextContent = textPattern.test(cleanedTable) + + // If table only has images/videos and no substantial text, remove it + return hasOnlyImagesOrVideos && !hasTextContent +} + +/** + * Clean external links - keep text but remove long URLs + */ +function cleanExternalLinks(text: string): string { + // Match markdown links: [text](url) + const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g + return text.replace(linkRegex, (match) => { + const linkMatch = match.match(/\[([^\]]+)\]\(([^)]+)\)/) + if (!linkMatch || !linkMatch[1] || !linkMatch[2]) { + return match + } + + const linkText = linkMatch[1] + const url = linkMatch[2] + + // Keep GitHub links (they're short and useful) + if (url.startsWith('https://github.com/') || url.startsWith('http://github.com/')) { + return match + } + + // Keep relative links + if (url.startsWith('/') || url.startsWith('./') || url.startsWith('../')) { + return match + } + + // Keep short URLs (< 50 chars) + if (url.length <= 50) { + return match + } + + // For long URLs, keep only the text + return linkText + }) +} + +/** + * Remove empty sections (headers with no content or only whitespace) + */ +function removeEmptySections(text: string): string { + // Match sections: ## Header followed by content until next ## or end + const sectionRegex = /(##+\s+[^\n]+)\n([\s\S]*?)(?=\n##+\s+|$)/g + let cleaned = text + + cleaned = cleaned.replace(sectionRegex, (match) => { + const sectionMatch = match.match(/(##+\s+[^\n]+)\n([\s\S]*?)(?=\n##+\s+|$)/) + if (!sectionMatch) { + return match + } + + const content = sectionMatch[2] || '' + // Check if content is empty or only whitespace + const trimmedContent = content.trim() + if (trimmedContent.length === 0) { + return '' // Remove empty section + } + return match // Keep section with content + }) + + return cleaned +} + +/** + * Remove minimal value sections (very short descriptions, redundant changes lists) + */ +function removeMinimalValueSections(text: string): string { + let cleaned = text + + // Remove "## Description" sections that are very short and redundant + const descriptionRegex = /##\s+Description\s*\n([\s\S]*?)(?=\n## |$)/gi + cleaned = cleaned.replace(descriptionRegex, (match) => { + const sectionMatch = match.match(/##\s+Description\s*\n([\s\S]*?)(?=\n## |$)/i) + if (!sectionMatch) { + return match + } + + const content = sectionMatch[1] || '' + const trimmedContent = content.trim() + + // Remove if very short (< 50 chars) and doesn't add value + if (trimmedContent.length < 50) { + return '' + } + + return match + }) + + // Remove "## Changes" sections that are just bullet lists without detail + const changesRegex = /##\s+Changes\s*\n([\s\S]*?)(?=\n## |$)/gi + cleaned = cleaned.replace(changesRegex, (match) => { + const sectionMatch = match.match(/##\s+Changes\s*\n([\s\S]*?)(?=\n## |$)/i) + if (!sectionMatch) { + return match + } + + const content = sectionMatch[1] || '' + const trimmedContent = content.trim() + + // Check if it's just a list of very short bullets + const lines = trimmedContent.split('\n').filter((line) => line.trim().length > 0) + const allShortBullets = lines.every((line) => { + const trimmed = line.trim() + return trimmed.startsWith('-') && trimmed.length < 80 + }) + + // Remove if all bullets are very short (likely redundant with CURSOR_SUMMARY) + if (allShortBullets && lines.length < 5) { + return '' + } + + return match + }) + + // Remove "## Implementation Details" that are redundant with CURSOR_SUMMARY + const implDetailsRegex = /##\s+Implementation Details\s*\n([\s\S]*?)(?=\n## |$)/gi + cleaned = cleaned.replace(implDetailsRegex, (match) => { + const sectionMatch = match.match(/##\s+Implementation Details\s*\n([\s\S]*?)(?=\n## |$)/i) + if (!sectionMatch) { + return match + } + + const content = sectionMatch[1] || '' + const trimmedContent = content.trim() + + // Remove if very short (< 100 chars) - likely redundant + if (trimmedContent.length < 100) { + return '' + } + + return match + }) + + return cleaned +} + +/** + * Remove redundant headers (headers with no meaningful content after them) + */ +function removeRedundantHeaders(text: string): string { + let cleaned = text + + // Match sections and check if header should be removed + const sectionRegex = /(##+\s+[^\n]+)\n([\s\S]*?)(?=\n##+\s+|$)/g + + cleaned = cleaned.replace(sectionRegex, (match) => { + const sectionMatch = match.match(/(##+\s+[^\n]+)\n([\s\S]*?)(?=\n##+\s+|$)/) + if (!sectionMatch) { + return match + } + + const header = sectionMatch[1] || '' + const content = sectionMatch[2] || '' + const trimmedContent = content.trim() + + // If header is followed immediately by CURSOR_SUMMARY-like content and nothing else + // Check if content starts with --- (CURSOR_SUMMARY marker pattern) + if (trimmedContent.startsWith('---') && trimmedContent.length < 200) { + // This might be redundant, but let's be conservative and keep it + return match + } + + // Remove headers that are redundant (e.g., "## Description" when content is minimal) + const headerLower = header.toLowerCase() + if (headerLower.includes('description') && trimmedContent.length < 30) { + // Keep the content, remove the header + return trimmedContent + } + + return match + }) + + return cleaned +} + +/** + * Remove redundant sections + */ +function removeRedundantSections(text: string): string { + let cleaned = text + + // Remove "Screen Captures" or "Screenshots" sections that only contain images + // Match: ## Screen Captures / ## Screenshots followed by content ending before next ## + const screenshotSectionRegex = /##\s+(Screen Captures|Screenshots|Screenshots?)\s*\n([\s\S]*?)(?=\n## |$)/gi + cleaned = cleaned.replace(screenshotSectionRegex, (match) => { + const sectionMatch = match.match(/##\s+(Screen Captures|Screenshots|Screenshots?)\s*\n([\s\S]*?)(?=\n## |$)/i) + if (!sectionMatch) { + return match + } + + const content = sectionMatch[2] || '' + // If content only has images/videos/tables with images, remove it + const mediaPattern = /!\[|\]\([^)]+\)|Screen Recording|\.mov|\.mp4| { + const sectionMatch = match.match(/##\s+(Testing|How Has This Been Tested\?)\s*\n([\s\S]*?)(?=\n## |$)/i) + if (!sectionMatch) { + return match + } + + const content = sectionMatch[2] || '' + const trimmedContent = content.trim() + + // Remove if empty + if (trimmedContent.length === 0) { + return '' + } + + // Remove if only checkboxes with no descriptions + const checkboxOnly = /^[\s-]*\[[ xX]\]/m.test(trimmedContent) && trimmedContent.length < 100 + if (checkboxOnly) { + return '' + } + + // Remove if content is just minimal phrases like "locally", "manually", "on simulator" + const minimalPhrases = + /^(locally|manually|on simulator|tested locally|tested manually|local|ios simulator|android sim)[\s.]*$/i + if (minimalPhrases.test(trimmedContent)) { + return '' + } + + // Remove if content is very short (< 50 chars) and doesn't contain meaningful text + const meaningfulTextPattern = /[a-zA-Z]{10,}/ + if (trimmedContent.length < 50 && !meaningfulTextPattern.test(trimmedContent)) { + return '' + } + + // Keep if it has meaningful content (> 100 chars of actual text) + const textOnly = trimmedContent.replace(/[\s\-[\]xX]/g, '') + if (textOnly.length < 100) { + return '' + } + + return match + }) + + return cleaned +} + +/** + * Normalize whitespace - aggressive cleanup + */ +function normalizeWhitespace(text: string): string { + // Collapse multiple blank lines to max 1 consecutive blank line + let cleaned = text.replace(/\n{3,}/g, '\n\n') + + // Trim trailing whitespace from lines + cleaned = cleaned + .split('\n') + .map((line) => line.trimEnd()) + .join('\n') + + // Remove leading/trailing blank lines + cleaned = cleaned.replace(/^\n+|\n+$/g, '') + + // Remove blank lines immediately after headers + cleaned = cleaned.replace(/(##+\s+[^\n]+)\n\n+/g, '$1\n') + + return cleaned +} diff --git a/apps/cli/src/lib/release-scanner.ts b/apps/cli/src/lib/release-scanner.ts new file mode 100644 index 00000000000..3a268135da2 --- /dev/null +++ b/apps/cli/src/lib/release-scanner.ts @@ -0,0 +1,283 @@ +/* eslint-disable no-console */ + +import type { Logger } from '@universe/cli/src/lib/logger' +import { $ } from 'bun' + +// ============================================================================ +// Types +// ============================================================================ + +export interface Release { + platform: 'mobile' | 'extension' + version: string + branch: string + major: number + minor: number + patch: number + prerelease?: string +} + +export interface ReleaseComparison { + from: Release + to: Release + commitRange: string +} + +// ============================================================================ +// Release Scanner +// ============================================================================ + +export class ReleaseScanner { + constructor( + private repoPath: string = process.cwd(), + private logger?: Logger, + ) {} + + /** + * Scan all release branches for a given platform + */ + async scanReleases(platform?: 'mobile' | 'extension'): Promise { + this.logger?.info(`Scanning for ${platform || 'all'} release branches...`) + + // Get all remote branches + const result = await $`git -C ${this.repoPath} branch -r`.text() + const branches = result + .split('\n') + .map((b: string) => b.trim()) + .filter(Boolean) + + // Filter for release branches + const releasePattern = platform ? `origin/releases/${platform}/` : 'origin/releases/' + + const releaseBranches = branches + .filter((b: string) => b.includes(releasePattern)) + .filter((b: string) => !b.includes('->')) // Exclude symbolic refs + .filter((b: string) => !b.includes('/dev')) // Exclude dev branches + .filter((b: string) => !b.match(/cherry|kickstart|mirror|temp|mp\//)) // Exclude special branches + + // Parse into Release objects + const releases: Release[] = [] + + for (const branch of releaseBranches) { + const release = this.parseReleaseBranch(branch) + if (release) { + releases.push(release) + } + } + + // Sort by version (newest first) + return releases.sort((a, b) => this.compareVersions(b, a)) + } + + /** + * Get the latest release for a platform + */ + async getLatestRelease(platform: 'mobile' | 'extension'): Promise { + const releases = await this.scanReleases(platform) + return releases[0] || null + } + + /** + * Get the previous release before a given version + */ + async getPreviousRelease(release: Release): Promise { + const releases = await this.scanReleases(release.platform) + + // Find the current release index + const currentIndex = releases.findIndex((r) => r.version === release.version && r.platform === release.platform) + + if (currentIndex === -1 || currentIndex === releases.length - 1) { + return null + } + + // Return the next one (which is older since we sorted newest first) + const nextRelease = releases[currentIndex + 1] + return nextRelease ?? null + } + + /** + * Find a specific release by platform and version + */ + async findRelease(platform: 'mobile' | 'extension', version: string): Promise { + const releases = await this.scanReleases(platform) + return releases.find((r) => r.version === version) || null + } + + async getReleaseComparison(args: { + platform: 'mobile' | 'extension' + version: string + compareWith?: string + }): Promise { + const { platform, version, compareWith } = args + const toRelease = await this.findRelease(platform, version) + if (!toRelease) { + throw new Error(`Release ${platform}/${version} not found`) + } + + let fromRelease: Release | null = null + + if (compareWith) { + fromRelease = await this.findRelease(platform, compareWith) + if (!fromRelease) { + throw new Error(`Release ${platform}/${compareWith} not found`) + } + } else { + // Auto-detect previous release + fromRelease = await this.getPreviousRelease(toRelease) + if (!fromRelease) { + this.logger?.warn(`No previous release found for ${platform}/${version}`) + return null + } + } + + // Use origin/ prefix for git commands + return { + from: fromRelease, + to: toRelease, + commitRange: `origin/${fromRelease.branch}..origin/${toRelease.branch}`, + } + } + + /** + * List releases in a formatted way + */ + async listReleases(platform?: 'mobile' | 'extension'): Promise { + const releases = await this.scanReleases(platform) + + if (releases.length === 0) { + console.log('No releases found') + return + } + + // Group by platform if showing all + const grouped = releases.reduce( + (acc, release) => { + if (!acc[release.platform]) { + acc[release.platform] = [] + } + const platformReleases = acc[release.platform] + if (platformReleases) { + platformReleases.push(release) + } + return acc + }, + {} as Record, + ) + + for (const [plat, rels] of Object.entries(grouped)) { + console.log(`\n${plat.toUpperCase()} RELEASES:`) + + console.log('─'.repeat(40)) + + for (const rel of rels.slice(0, 10)) { + // Show only latest 10 + + console.log(` ${rel.version.padEnd(10)} → ${rel.branch}`) + } + + if (rels.length > 10) { + console.log(` ... and ${rels.length - 10} more`) + } + } + } + + /** + * Get commits between two releases + */ + async getReleaseCommits(comparison: ReleaseComparison): Promise { + const result = await $`git -C ${this.repoPath} log ${comparison.commitRange} --oneline`.text() + return result + } + + /** + * Parse a release branch name into a Release object + */ + private parseReleaseBranch(branch: string): Release | null { + // Match patterns like: + // origin/releases/mobile/1.60 + // origin/releases/extension/1.30.0 + // Safe regex pattern - matches controlled git branch names only + // eslint-disable-next-line security/detect-unsafe-regex -- Controlled pattern matching git branch names with bounded quantifiers + const match = branch.match(/^origin\/releases\/(mobile|extension)\/(\d+)\.(\d+)(?:\.(\d+))?(?:\.(.+))?$/) + + if (!match) { + return null + } + + const [, platform, major, minor, patch, prerelease] = match + if (!platform || !major || !minor) { + return null + } + const version = patch + ? `${major}.${minor}.${patch}${prerelease ? `.${prerelease}` : ''}` + : `${major}.${minor}${prerelease ? `.${prerelease}` : ''}` + + return { + platform: platform as 'mobile' | 'extension', + version, + branch: branch.replace('origin/', ''), + major: parseInt(major, 10), + minor: parseInt(minor, 10), + patch: parseInt(patch ?? '0', 10), + prerelease: prerelease ?? undefined, + } + } + + /** + * Compare two versions (returns positive if a > b, negative if a < b, 0 if equal) + */ + private compareVersions(a: Release, b: Release): number { + // Compare major + if (a.major !== b.major) { + return a.major - b.major + } + + // Compare minor + if (a.minor !== b.minor) { + return a.minor - b.minor + } + + // Compare patch + if (a.patch !== b.patch) { + return a.patch - b.patch + } + + // Compare prerelease (if both have it) + if (a.prerelease && b.prerelease) { + return a.prerelease.localeCompare(b.prerelease) + } + + // Version without prerelease is greater than with prerelease + if (a.prerelease && !b.prerelease) { + return -1 + } + if (!a.prerelease && b.prerelease) { + return 1 + } + + return 0 + } +} + +/** + * Parse a release identifier like "mobile/1.60" or "extension/1.30.0" + */ +export function parseReleaseIdentifier( + identifier: string, +): { platform: 'mobile' | 'extension'; version: string } | null { + const match = identifier.match(/^(mobile|extension)\/(.+)$/) + if (!match) { + return null + } + + const platform = match[1] as 'mobile' | 'extension' + const version = match[2] + if (!version) { + return null + } + + return { + platform, + version, + } +} diff --git a/apps/cli/src/lib/stream-handler.ts b/apps/cli/src/lib/stream-handler.ts new file mode 100644 index 00000000000..9a262b12e25 --- /dev/null +++ b/apps/cli/src/lib/stream-handler.ts @@ -0,0 +1,35 @@ +/** biome-ignore-all lint/suspicious/noConsole: cli tool */ +import type { StreamChunk } from '@universe/cli/src/lib/ai-provider' + +/** + * Stream Handler for CLI Output + * + * Separates the concern of handling streaming output to console. + * Accumulates full response while streaming to stdout for user feedback. + */ + +/** + * Writes stream chunks to console and accumulates full response + * @param stream - Async generator of stream chunks + * @returns Complete accumulated text + */ +export async function writeStreamToConsole(stream: AsyncGenerator): Promise { + let fullText = '' + + for await (const chunk of stream) { + if (chunk.text) { + fullText += chunk.text + // Write to stdout for real-time feedback + process.stdout.write(chunk.text) + } + + if (chunk.isComplete) { + // New line after streaming completes + // eslint-disable-next-line no-console + console.log() + break + } + } + + return fullText +} diff --git a/apps/cli/src/lib/team-members.ts b/apps/cli/src/lib/team-members.ts new file mode 100644 index 00000000000..91a7fb39f04 --- /dev/null +++ b/apps/cli/src/lib/team-members.ts @@ -0,0 +1,37 @@ +import { $ } from 'bun' + +export interface TeamMember { + login: string + name: string | null +} + +/** + * Fetches team members from a GitHub organization team + * Returns members with their login and display name + */ +export async function fetchTeamMembers(org: string, teamSlug: string): Promise { + try { + // Get team members (just logins) + const membersResult = await $`gh api /orgs/${org}/teams/${teamSlug}/members --jq '.[].login'`.text() + const logins = membersResult.split('\n').filter(Boolean) + + // Fetch detailed user info for each member + const members: TeamMember[] = [] + for (const login of logins) { + try { + const userResult = await $`gh api /users/${login} --jq '{login: .login, name: .name}'`.text() + const userData = JSON.parse(userResult) as TeamMember + members.push(userData) + } catch { + // If fetching user details fails, just use the login + members.push({ login, name: null }) + } + } + + return members + } catch (error) { + throw new Error(`Failed to fetch members for team ${teamSlug}. Ensure gh CLI is authenticated and team exists.`, { + cause: error, + }) + } +} diff --git a/apps/cli/src/lib/team-resolver.ts b/apps/cli/src/lib/team-resolver.ts new file mode 100644 index 00000000000..b765f94c146 --- /dev/null +++ b/apps/cli/src/lib/team-resolver.ts @@ -0,0 +1,156 @@ +import { $ } from 'bun' + +interface UserResolution { + username?: string + emails: string[] +} + +interface GitHubCommitSearchItem { + commit?: { + author?: { + email?: string + } + } +} + +interface GitHubCommitSearchResult { + items?: GitHubCommitSearchItem[] +} + +interface GitHubUserData { + id?: number + email?: string + login?: string +} + +/** + * Resolves a team reference or username to email addresses + * Supports: + * - GitHub teams: @org/team + * - GitHub usernames: alice, bob + * - Email addresses: alice@example.com + * - Mixed: @org/team,alice,bob@example.com + */ +export async function resolveTeam(teamRef: string): Promise<{ emails: string[]; usernames: string[] }> { + const parts = teamRef + .split(',') + .map((p) => p.trim()) + .filter(Boolean) + const allEmails: string[] = [] + const allUsernames: string[] = [] + + for (const part of parts) { + if (part.startsWith('@')) { + // GitHub team reference + const { emails, usernames } = await resolveGitHubTeam(part) + allEmails.push(...emails) + allUsernames.push(...usernames) + } else if (part.includes('@')) { + // Already an email + allEmails.push(part) + } else { + // GitHub username + const resolution = await resolveUserToEmail(part) + allEmails.push(...resolution.emails) + if (resolution.username) { + allUsernames.push(resolution.username) + } + } + } + + return { + emails: [...new Set(allEmails)], // Remove duplicates + usernames: [...new Set(allUsernames)], + } +} + +async function resolveGitHubTeam(teamRef: string): Promise<{ emails: string[]; usernames: string[] }> { + // Parse @org/team format + const [org, team] = teamRef.slice(1).split('/') + + if (!org || !team) { + throw new Error(`Invalid team reference: ${teamRef}. Expected format: @org/team`) + } + + try { + // Get team members + const membersResult = await $`gh api /orgs/${org}/teams/${team}/members --jq '.[].login'`.text() + const members = membersResult.split('\n').filter(Boolean) + + // Resolve each member to emails + const emails: string[] = [] + const usernames: string[] = [] + + for (const member of members) { + const resolution = await resolveUserToEmail(member) + emails.push(...resolution.emails) + if (resolution.username) { + usernames.push(resolution.username) + } + } + + return { emails, usernames } + } catch (_error) { + throw new Error(`Failed to resolve team ${teamRef}. Ensure gh CLI is authenticated and team exists.`) + } +} + +async function resolveUserToEmail(user: string): Promise { + // If it contains @, it's already an email + if (user.includes('@')) { + return { emails: [user] } + } + + // Otherwise, treat it as a GitHub username + try { + const userDataResult = await $`gh api /users/${user}`.text() + const userData = JSON.parse(userDataResult) as GitHubUserData + + // Get user's email from their profile (if public) + const emails: string[] = [] + if (userData.email) { + emails.push(userData.email) + } + + // Also try to get commit email by searching for their commits + try { + const searchResult = await $`gh api /search/commits?q=author:${user}&per_page=5`.text() + const searchData = JSON.parse(searchResult) as GitHubCommitSearchResult + + if (searchData.items && searchData.items.length > 0) { + const commitEmails = searchData.items + .map((item) => item.commit?.author?.email) + .filter((email): email is string => typeof email === 'string' && !emails.includes(email)) + + emails.push(...commitEmails) + } + } catch { + // Commit search failed, continue with what we have + } + + if (emails.length === 0 && userData.id) { + // Fallback: use GitHub's noreply email format + emails.push(`${userData.id}+${user}@users.noreply.github.com`) + } + + return { username: user, emails } + } catch (_error) { + throw new Error(`Failed to resolve GitHub username "${user}". User may not exist or gh CLI is not configured.`) + } +} + +/** + * Detects repository from git remote + */ +export async function detectRepository(): Promise<{ owner: string; name: string } | null> { + try { + const remote = await $`git config --get remote.origin.url`.text() + const match = remote.trim().match(/github\.com[:/]([^/]+)\/([^/.]+)(\.git)?$/) + if (match?.[1] && match[2]) { + return { owner: match[1], name: match[2] } + } + } catch { + // Not a git repo or no remote configured + } + return null +} diff --git a/apps/cli/src/lib/trivial-files.ts b/apps/cli/src/lib/trivial-files.ts new file mode 100644 index 00000000000..cd4b0b814d9 --- /dev/null +++ b/apps/cli/src/lib/trivial-files.ts @@ -0,0 +1,74 @@ +/** + * Utility for identifying trivial files that should be filtered from changelog analysis + */ + +const TRIVIAL_PATTERNS = [ + // Lockfiles + /package-lock\.json$/, + /yarn\.lock$/, + /pnpm-lock\.yaml$/, + /bun\.lockb$/, + /Cargo\.lock$/, + /Gemfile\.lock$/, + /composer\.lock$/, + /poetry\.lock$/, + + // Snapshots + /\.snap$/, + /\.snap\.\w+$/, + /\/__snapshots__\//, + /\.snapshot$/, + /\.snapshot\.json$/, + + // Generated files + /\.generated\./, + /\/__generated__\//, + /\/generated\//, + /codegen\//, + + // Build artifacts + /^dist\//, + /^build\//, + /\.next\//, + /\.turbo\//, + /^out\//, + + // Test artifacts + /^coverage\//, + /\.lcov$/, + /\.nyc_output\//, + /test-results\//, + + // Large data files + /fixtures\//, + /\/__fixtures__\//, + /testdata\//, + + // Binary and media files + /\.(png|jpg|jpeg|gif|ico|svg|webp|pdf|zip|tar|gz)$/i, + + // IDE and OS files + /\.DS_Store$/, + /Thumbs\.db$/, + /\.swp$/, + /\.swo$/, + + // Other + /node_modules\//, + /vendor\//, + /\.pnp\./, +] + +/** + * Check if a file path represents a trivial file that should be filtered + */ +export function isTrivialFile(path: string): boolean { + return TRIVIAL_PATTERNS.some((pattern) => pattern.test(path)) +} + +/** + * Filter an array of file paths to exclude trivial files + */ +export function filterTrivialFiles(paths: string[]): string[] { + return paths.filter((path) => !isTrivialFile(path)) +} diff --git a/apps/cli/src/prompts/bug-bisect.md b/apps/cli/src/prompts/bug-bisect.md new file mode 100644 index 00000000000..fbec2db123e --- /dev/null +++ b/apps/cli/src/prompts/bug-bisect.md @@ -0,0 +1,83 @@ +You are analyzing commits in a release to identify which commit likely introduced a bug. Your goal is to carefully examine all commits and PRs in the release range and rank them by likelihood of introducing the bug described. + +## Bug Description + +{{BUG_DESCRIPTION}} + +## Release Context + +- **Platform:** {{PLATFORM}} +- **Release:** {{RELEASE_TO}} +- **Comparing with:** {{RELEASE_FROM}} + +## Your Task + +Analyze ALL commits and pull requests in the release range. For each commit/PR, determine how likely it is that it introduced the bug described above. Consider: + +1. **Direct relevance**: Does the commit modify code that directly relates to the bug description? +2. **Indirect impact**: Could changes in this commit cause side effects that lead to the bug? +3. **Pattern matching**: Do file paths, function names, or component names match keywords in the bug description? +4. **Timing**: If the bug appeared in this release, commits in this range are prime suspects +5. **Related PRs**: Multiple commits from the same PR may be related and should be considered together + +## Output Format + +You MUST return a valid JSON object with the following structure: + +```json +{ + "suspiciousCommits": [ + { + "sha": "full commit SHA", + "confidence": 0.85, + "reasoning": "Brief explanation of why this commit is suspicious, mentioning specific files/functions/modules changed that relate to the bug", + "relatedPR": 1234 + } + ], + "summary": "Brief summary of findings: how many commits analyzed, how many suspicious commits found, and overall assessment", + "totalCommitsAnalyzed": 247, + "releaseContext": { + "from": "{{RELEASE_FROM}}", + "to": "{{RELEASE_TO}}", + "platform": "{{PLATFORM}}" + } +} +``` + +## Requirements + +1. **Rank commits by confidence**: Order `suspiciousCommits` array from highest to lowest confidence +2. **Confidence scores**: Use 0.0-1.0 scale where: + - 0.9-1.0: Very likely culprit (direct match, clear causation) + - 0.7-0.9: Likely related (strong indirect connection) + - 0.5-0.7: Possibly related (weak connection, worth investigating) + - < 0.5: Unlikely (exclude from results) +3. **Return top 10-20 commits**: Focus on the most suspicious commits, not all commits +4. **Include reasoning**: Each commit must have a clear explanation of why it's suspicious +5. **Match PRs**: If a commit is part of a PR, include the PR number in `relatedPR` +6. **Be specific**: Reference specific files, functions, or components in your reasoning + +## Analysis Process + +Before generating your output, analyze the commit data systematically: + +1. **Scan for keywords**: Look for file paths, function names, or component names that match keywords in the bug description +2. **Review PR descriptions**: PR bodies often contain context about what changed and why +3. **Check related commits**: Commits that touch similar files or components may be related +4. **Consider the full context**: Sometimes the bug is caused by an interaction between multiple changes + +## Important Notes + +- Analyze ALL commits provided, even if the context is truncated due to token limits +- If multiple commits from the same PR are suspicious, include them all but note they're related +- Be thorough but focused - prioritize commits with the strongest connection to the bug +- Your reasoning should help developers quickly understand why each commit is suspicious + +Here is the commit data you need to analyze: + + +{{COMMIT_DATA}} + + +Now analyze the commits and return your JSON response with ranked suspicious commits. + diff --git a/apps/cli/src/prompts/release-changelog.md b/apps/cli/src/prompts/release-changelog.md new file mode 100644 index 00000000000..d1306c92c52 --- /dev/null +++ b/apps/cli/src/prompts/release-changelog.md @@ -0,0 +1,74 @@ +You are creating a technical changelog for engineers at Uniswap Labs. Your goal is to analyze commit data and write a changelog that explains what shipped to colleagues in a direct, conversational, and factual manner - similar to Linear's changelog style. + +**CRITICAL: Focus on what actually changed, not editorial judgments** +- Describe the specific changes that were made +- Avoid inferential language like "finally," "now works properly," or "is finally real" +- Don't make assumptions about timeline, quality, or significance +- State what changed factually without commentary + +Before writing the changelog, do your analysis and planning work in tags inside your thinking block. It's OK for this section to be quite long. Include: + +1. **Commit Extraction**: First, go through the commit data and list out all the key commits with their PR numbers and descriptions to keep them top of mind. + +2. **Pattern Identification**: Read through all the commits and identify major themes or areas of work (aim for 4-7 themes). Look for related PRs that address similar functionality, components, or types of changes. + +3. **Grouping Strategy**: For each theme you identify, list which specific PRs belong to it and what the common thread is. + +4. **Factual Focus**: For each group, identify the specific technical changes made without making inferences about their importance or timeline. + +5. **Structure Planning**: Plan how you'll organize each theme section and what specific language you'll use - some may need more technical detail, others may be straightforward. + +After your planning, write the changelog using this exact structure: + +## Release Overview +- [X] PRs, [Y] contributors +- [One paragraph listing main work areas. Keep it factual and brief.] + +## Major Themes + +For each major pattern (4-7 themes total): + +### [Direct, Clear Theme Name] + +[2-3 paragraphs explaining what changed. Focus on the actual modifications made to the codebase. Vary your structure - not every section needs the same format. Mix technical details with broader changes as appropriate.] + +
+All related PRs (X total) + +- #123: Brief description +- #124: What changed +- #125: Technical detail +[... all PRs for this theme ...] +
+ +**Contributors:** @name1, @name2, @name3 + +**Writing Guidelines:** +- Write like a human, not a content generator +- Vary sentence starters - don't always begin with "The team" or "This release" +- Use present tense for current state: "The extension locks automatically after..." +- Be conversational but professional: "The old `isAddress()` function is gone - replaced with explicit validators" +- Include technical specifics when developers would care about them (function names, specific fixes, workarounds) +- Natural transitions or none at all - sometimes jump between topics +- Some features get detailed explanations, others just need a line or two + +**Avoid:** +- Editorial language: "smart decisions," "clever solutions," "finally," "now works properly" +- Justification: "This is important because..." +- Hedging: "presumably," "apparently" +- Obvious transitions: "Worth noting," "It's important to mention," "The interesting part" + +**Include When Relevant:** +- Specific function names, APIs, or technical components that changed +- Workarounds or gotchas developers should know about +- Patterns that are being reused across the codebase +- Platform-specific considerations +- Breaking changes or migrations + +Your final output should consist only of the changelog in the specified format and should not duplicate or rehash any of the analysis and planning work you did in the thinking block. + +Here is the commit data you need to analyze: + + +{{COMMIT_DATA}} + diff --git a/apps/cli/src/prompts/team-digest.md b/apps/cli/src/prompts/team-digest.md new file mode 100644 index 00000000000..d3271fa0c53 --- /dev/null +++ b/apps/cli/src/prompts/team-digest.md @@ -0,0 +1,102 @@ +You will analyze repository commit data to create a team digest for engineers at Uniswap Labs. Your goal is to transform raw development activity into a readable summary that explains what the team accomplished during a specific time period. + +## Your Task + +Create a structured team digest that describes what was built, added, or modified based on the commit data. Write factually about the work completed without making assumptions about completion status or quality. + +## Critical Requirements + +Follow these requirements strictly: + +1. **Use GitHub usernames only** - Format as @username, never use or invent human names +2. **Describe changes factually** - State what was built/added/modified without making quality judgments +3. **Avoid completion language** - Do not use "complete," "finished," "fully implemented," or "comprehensive" since you cannot determine completion status from commit messages alone +4. **Avoid embellished language** - Do not use "significantly improved," "enhanced," or "optimized" unless these are objectively measurable facts explicitly stated in the commit messages +5. **Right-size technical detail** - Include key components and patterns, skip implementation minutiae +6. **Focus on outcomes first** - Lead with what changed for users/developers, then explain how +7. **Minimize code content** - Be very selective about including code snippets or technical implementation details. Focus on impact and readability. Only include essential code-related information when truly necessary for understanding the work's significance +8. **Improve readability** - Limit to 4-6 themes maximum to avoid monotony. Vary your sentence structure and integrate contributor mentions naturally rather than starting every paragraph with "@person did xyz" + +## Analysis Process + +Before writing your digest, complete a thorough analysis inside tags within your thinking block. It's OK for this section to be quite long. Include: + +1. **Extract All Commit Messages**: Quote every single commit message verbatim from the data, one by one +2. **Extract Contributors**: Systematically go through the data and list all GitHub usernames (format as @username), counting them as you go +3. **Group into 4-6 Themes**: Organize commits into coherent themes based on functionality or area of work. For each theme: + - Theme name + - Specific commits that belong (quote relevant messages verbatim) + - Contributors who worked on it +4. **Plan Technical Details**: For each theme, identify: + - Main outcome or change (focus on impact, not implementation) + - 1-2 key technical details worth mentioning only if they help explain impact + - Avoid code snippets unless absolutely essential +5. **Structure Planning**: Write your planned theme names and brief descriptions +6. **Readability Check**: Review your planned descriptions to ensure you: + - Have 4-6 themes maximum for better flow + - Vary sentence structure (don't start every paragraph with contributor names) + - Integrate contributor mentions naturally throughout the narrative + - State facts from commits without making quality assessments + - Avoid completion language + - Focus on outcomes over technical implementation +7. **Requirements Verification**: Systematically check each planned theme description against all 8 critical requirements listed above, going through them one by one to ensure compliance + +## Output Structure + +Write your digest following this exact structure: + +### Team Digest: [Date Range] + +[Opening paragraph: Main areas of work. Be specific but concise - 2-3 sentences maximum.] + +### What Was Shipped + +For each theme (4-6 total): + +#### [Clear Theme Name] + +[2-3 paragraphs explaining: +- Paragraph 1: What was built and why it matters (outcome and impact) +- Paragraph 2: Key approach - mention important components only when relevant for understanding impact +- Paragraph 3 (if needed): Integration points or collaboration details + +Mention contributors naturally throughout, varying sentence structure] + +### Technical Highlights + +[3-5 bullet points of technically interesting work that other engineers would want to know about. Focus on reusable patterns, architectural decisions, or important changes. Only include items that add significant value and aren't duplicative of what was stated above.] + +## Example Output Structure + +### Team Digest: March 1-15, 2024 + +The team focused on expanding notification capabilities, refactoring data layer components, and improving test infrastructure. + +### What Was Shipped + +#### Notification System Development + +A new notification system now polls the backend and manages dismissal state locally, allowing users to receive timely updates about important events. The system provides options for different notification types and integrates with existing user preferences through the settings panel. + +Working primarily on the core functionality, @alice built the polling mechanism and state management, while @bob handled the integration work that allows users to configure which notifications they receive and how they're delivered. + +#### Data Layer Refactoring + +The data access layer underwent restructuring with multiple API clients consolidated into a unified service. This change provides more consistent error handling and simplifies how the application manages data across different features. + +@charlie led this refactoring effort, which resulted in a unified error handling pattern that reduces code duplication across components. + +### Technical Highlights + +• New notification provider can be easily integrated into other applications +• Unified error handling pattern reduces code duplication across components +• Test suite now runs 40% faster due to infrastructure improvements +• New data fetching approach simplifies component logic and improves testability + +Your final output should consist only of the structured team digest following the format above, without duplicating or rehashing any of the analysis work you completed in your thinking block. + +Here is the commit data you need to analyze: + + +{{COMMIT_DATA}} + diff --git a/apps/cli/src/ui/App.tsx b/apps/cli/src/ui/App.tsx new file mode 100644 index 00000000000..e0ddb98230c --- /dev/null +++ b/apps/cli/src/ui/App.tsx @@ -0,0 +1,175 @@ +import type { OrchestratorConfig } from '@universe/cli/src/core/orchestrator' +import type { Release } from '@universe/cli/src/lib/release-scanner' +import { AppStateProvider, type TeamFilter, useAppState } from '@universe/cli/src/ui/hooks/useAppState' +import { BugBisectResultsScreen } from '@universe/cli/src/ui/screens/BugBisectResultsScreen' +import { BugInputScreen } from '@universe/cli/src/ui/screens/BugInputScreen' +import { ConfigReview } from '@universe/cli/src/ui/screens/ConfigReview' +import { ExecutionScreen } from '@universe/cli/src/ui/screens/ExecutionScreen' +import { ReleaseSelector } from '@universe/cli/src/ui/screens/ReleaseSelector' +import { ResultsScreen } from '@universe/cli/src/ui/screens/ResultsScreen' +import { TeamSelectorScreen } from '@universe/cli/src/ui/screens/TeamSelectorScreen' +import { WelcomeScreen } from '@universe/cli/src/ui/screens/WelcomeScreen' +import { useCallback, useEffect } from 'react' + +function AppContent(): JSX.Element { + const { state, dispatch } = useAppState() + + // Clear terminal on initial mount + useEffect(() => { + process.stdout.write('\x1Bc') // VT100 clear screen and scrollback + }, []) + const handleWelcomeContinue = (mode: 'release-changelog' | 'team-digest' | 'changelog' | 'bug-bisect'): void => { + // Route based on analysis mode + switch (mode) { + case 'release-changelog': + dispatch({ type: 'SET_SCREEN', screen: 'release-select' }) + break + case 'bug-bisect': + dispatch({ type: 'SET_SCREEN', screen: 'release-select' }) + break + case 'team-digest': + dispatch({ type: 'SET_SCREEN', screen: 'team-select' }) + break + case 'changelog': + // Skip to config review for custom analysis + dispatch({ type: 'SET_SCREEN', screen: 'config-review' }) + break + } + } + + const handleReleaseSelect = (_release: Release, _comparison: Release | null): void => { + // If bug-bisect mode, go to bug input screen; otherwise go to config review + if (state.analysisMode === 'bug-bisect') { + dispatch({ type: 'SET_SCREEN', screen: 'bug-input' }) + } else { + dispatch({ type: 'SET_SCREEN', screen: 'config-review' }) + } + } + + const handleBugInputContinue = (): void => { + dispatch({ type: 'SET_SCREEN', screen: 'config-review' }) + } + + const handleBugInputBack = (): void => { + dispatch({ type: 'SET_SCREEN', screen: 'release-select' }) + } + + const handleTeamSelect = (_teamFilter: TeamFilter | null): void => { + dispatch({ type: 'SET_SCREEN', screen: 'config-review' }) + } + + const handleConfigConfirm = (config: OrchestratorConfig): void => { + dispatch({ type: 'UPDATE_CONFIG', config }) + dispatch({ type: 'SET_SCREEN', screen: 'execution' }) + } + + const handleExecutionComplete = useCallback( + (results: Record): void => { + // Transform orchestrator results to app state format + // Orchestrator returns { analysis: "markdown content", ... } or JSON for bug-bisect + const changelog = typeof results.analysis === 'string' ? results.analysis : JSON.stringify(results, null, 2) + + dispatch({ type: 'SET_RESULTS', results: { changelog, metadata: results } }) + // Route to bug-bisect results screen if in bug-bisect mode + if (state.analysisMode === 'bug-bisect') { + dispatch({ type: 'SET_SCREEN', screen: 'results' }) + } else { + dispatch({ type: 'SET_SCREEN', screen: 'results' }) + } + }, + [dispatch, state.analysisMode], + ) + + const handleExecutionError = useCallback( + (_error: Error): void => { + dispatch({ type: 'SET_EXECUTION_STATE', state: 'error' }) + // Could navigate to error screen or show error in execution screen + }, + [dispatch], + ) + + const handleBack = (): void => { + // Determine previous screen based on current screen and mode + if (state.screen === 'config-review') { + // Go back to appropriate selector based on mode + switch (state.analysisMode) { + case 'release-changelog': + dispatch({ type: 'SET_SCREEN', screen: 'release-select' }) + break + case 'bug-bisect': + dispatch({ type: 'SET_SCREEN', screen: 'bug-input' }) + break + case 'team-digest': + dispatch({ type: 'SET_SCREEN', screen: 'team-select' }) + break + case 'changelog': + dispatch({ type: 'SET_SCREEN', screen: 'welcome' }) + break + } + } else if (state.screen === 'bug-input') { + dispatch({ type: 'SET_SCREEN', screen: 'release-select' }) + } else if (state.screen === 'release-select' || state.screen === 'team-select') { + dispatch({ type: 'SET_SCREEN', screen: 'welcome' }) + } else if (state.screen === 'execution' || state.screen === 'results') { + dispatch({ type: 'SET_SCREEN', screen: 'config-review' }) + } + } + + const handleRestart = (): void => { + dispatch({ type: 'SET_SCREEN', screen: 'welcome' }) + dispatch({ type: 'SELECT_RELEASE', release: null }) + dispatch({ type: 'SET_COMPARISON_RELEASE', release: null }) + dispatch({ type: 'SET_RESULTS', results: null }) + dispatch({ type: 'SET_EXECUTION_STATE', state: 'idle' }) + } + + switch (state.screen) { + case 'welcome': + return + case 'release-select': + return + case 'bug-input': + return + case 'team-select': + return + case 'config-review': + return + case 'execution': + return ( + + ) + case 'results': + // Use BugBisectResultsScreen for bug-bisect mode, otherwise ResultsScreen + if (state.analysisMode === 'bug-bisect') { + return ( + + ) + } + return ( + + ) + default: + return + } +} + +export function App(): JSX.Element { + return ( + + + + ) +} diff --git a/apps/cli/src/ui/components/Banner.tsx b/apps/cli/src/ui/components/Banner.tsx new file mode 100644 index 00000000000..09c3d17375a --- /dev/null +++ b/apps/cli/src/ui/components/Banner.tsx @@ -0,0 +1,30 @@ +import { Box, Text } from 'ink' +import Gradient from 'ink-gradient' + +const BANNER_ART = ` + ██╗ ██╗███╗ ██╗██╗██╗ ██╗███████╗██████╗ ███████╗███████╗ + ██║ ██║████╗ ██║██║██║ ██║██╔════╝██╔══██╗██╔════╝██╔════╝ + ██║ ██║██╔██╗ ██║██║██║ ██║█████╗ ██████╔╝███████╗█████╗ + ██║ ██║██║╚██╗██║██║╚██╗ ██╔╝██╔══╝ ██╔══██╗╚════██║██╔══╝ + ╚██████╔╝██║ ╚████║██║ ╚████╔╝ ███████╗██║ ██║███████║███████╗ (cli) + ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝╚══════╝╚══════╝ +` + +interface BannerProps { + subtitle?: string +} + +export function Banner({ subtitle }: BannerProps): JSX.Element { + return ( + + {BANNER_ART} + {subtitle && ( + + + {subtitle} + + + )} + + ) +} diff --git a/apps/cli/src/ui/components/Box.tsx b/apps/cli/src/ui/components/Box.tsx new file mode 100644 index 00000000000..e988e17715f --- /dev/null +++ b/apps/cli/src/ui/components/Box.tsx @@ -0,0 +1,18 @@ +import { Box as InkBox, Text } from 'ink' +import type { ReactNode } from 'react' + +interface BoxProps { + children: ReactNode + title?: string + borderColor?: string + padding?: number +} + +export function Box({ children, title, borderColor, padding = 1 }: BoxProps): JSX.Element { + return ( + + {title && {title}} + {children} + + ) +} diff --git a/apps/cli/src/ui/components/ChangelogPreview.tsx b/apps/cli/src/ui/components/ChangelogPreview.tsx new file mode 100644 index 00000000000..48315cd9a02 --- /dev/null +++ b/apps/cli/src/ui/components/ChangelogPreview.tsx @@ -0,0 +1,20 @@ +import { Box as InkBox, Text } from 'ink' + +interface ChangelogPreviewProps { + changelog: string +} + +export function ChangelogPreview({ changelog }: ChangelogPreviewProps): JSX.Element { + // Split into lines and render + const lines = changelog.split('\n') + + return ( + + Changelog Preview + + {lines.map((line, index) => ( + {line} + ))} + + ) +} diff --git a/apps/cli/src/ui/components/FormField.tsx b/apps/cli/src/ui/components/FormField.tsx new file mode 100644 index 00000000000..68f2aaaca78 --- /dev/null +++ b/apps/cli/src/ui/components/FormField.tsx @@ -0,0 +1,26 @@ +import { Box, Text } from 'ink' +import type { ReactNode } from 'react' + +interface FormFieldProps { + children: ReactNode + focused?: boolean + helpText?: string + marginLeft?: number +} + +/** + * Wrapper component for form fields that provides consistent styling + * and focus handling. Composable with Toggle, TextInput, NumberInput, etc. + */ +export function FormField({ children, focused, helpText, marginLeft = 0 }: FormFieldProps): JSX.Element { + return ( + + {children} + {helpText && focused && ( + + {helpText} + + )} + + ) +} diff --git a/apps/cli/src/ui/components/NumberInput.tsx b/apps/cli/src/ui/components/NumberInput.tsx new file mode 100644 index 00000000000..9e327e9ad9d --- /dev/null +++ b/apps/cli/src/ui/components/NumberInput.tsx @@ -0,0 +1,39 @@ +import { colors } from '@universe/cli/src/ui/utils/colors' +import { Box, Text } from 'ink' + +interface NumberInputProps { + label: string + value: number + onChange: (value: number) => void + focused?: boolean + isEditing?: boolean + editValue?: string + min?: number + max?: number + step?: number +} + +export function NumberInput({ + label, + value, + onChange: _onChange, + focused = false, + isEditing = false, + editValue, + min: _min, + max: _max, + step: _step = 1, +}: NumberInputProps): JSX.Element { + const displayValue = isEditing && editValue !== undefined ? editValue : value + + return ( + + + {focused ? '❯ ' : ' '} + {label}: {displayValue} + {isEditing && (Enter to save, Esc to cancel, type digits)} + {focused && !isEditing && (↑↓←→ to adjust, Enter to edit)} + + + ) +} diff --git a/apps/cli/src/ui/components/ProgressIndicator.tsx b/apps/cli/src/ui/components/ProgressIndicator.tsx new file mode 100644 index 00000000000..4b7a3f75344 --- /dev/null +++ b/apps/cli/src/ui/components/ProgressIndicator.tsx @@ -0,0 +1,75 @@ +import type { ProgressEvent, ProgressStage } from '@universe/cli/src/ui/services/orchestrator-service' +import { colors } from '@universe/cli/src/ui/utils/colors' +import { Box, Text } from 'ink' + +interface Stage { + key: ProgressStage + label: string +} + +const stages: Stage[] = [ + { key: 'collecting', label: 'Collecting data' }, + { key: 'analyzing', label: 'Analyzing with AI' }, + { key: 'delivering', label: 'Delivering results' }, +] + +interface ProgressIndicatorProps { + currentStage: ProgressStage + message?: string + cacheInfo?: ProgressEvent['cacheInfo'] +} + +export function ProgressIndicator({ currentStage, message, cacheInfo }: ProgressIndicatorProps): JSX.Element { + const currentIndex = stages.findIndex((s) => s.key === currentStage) + + const getCacheLabel = (cacheInfoItem: ProgressEvent['cacheInfo']): string => { + if (!cacheInfoItem) { + return '' + } + const typeLabel = cacheInfoItem.type === 'commits' ? 'commits' : cacheInfoItem.type === 'prs' ? 'PRs' : 'stats' + return `(cached: ${cacheInfoItem.count} ${typeLabel})` + } + + return ( + + {stages.map((stage, index) => { + const isComplete = index < currentIndex + const isCurrent = index === currentIndex && currentStage !== 'idle' && currentStage !== 'error' + + let icon = '○' + let color = 'gray' + + if (isComplete) { + icon = '●' + color = 'green' + } else if (isCurrent) { + icon = '◉' + color = colors.primary + } + + // Show cache info for collecting stage when it's current or complete + const showCacheInfo = cacheInfo && stage.key === 'collecting' && (isCurrent || isComplete) + + return ( + + {icon} {stage.label} + {showCacheInfo && ( + + {' '} + {getCacheLabel(cacheInfo)} + + )} + {isCurrent && !showCacheInfo && '...'} + + ) + })} + {message && ( + + + {message} + + + )} + + ) +} diff --git a/apps/cli/src/ui/components/ReleaseList.tsx b/apps/cli/src/ui/components/ReleaseList.tsx new file mode 100644 index 00000000000..04911f578fa --- /dev/null +++ b/apps/cli/src/ui/components/ReleaseList.tsx @@ -0,0 +1,34 @@ +import type { Release } from '@universe/cli/src/lib/release-scanner' +import { colors } from '@universe/cli/src/ui/utils/colors' +import { formatBranch } from '@universe/cli/src/ui/utils/format' +import { Text } from 'ink' + +interface ReleaseListProps { + releases: Release[] + selectedIndex: number | null + platform?: 'mobile' | 'extension' +} + +export function ReleaseList({ releases, selectedIndex, platform }: ReleaseListProps): JSX.Element { + const filtered = platform ? releases.filter((r) => r.platform === platform) : releases + + if (filtered.length === 0) { + return No releases found + } + + return ( + <> + {filtered.map((release, index) => { + const isSelected = selectedIndex === index + const prefix = isSelected ? '→ ' : ' ' + + return ( + + {prefix} + {release.platform}/{release.version} ({formatBranch(release.branch)}) + + ) + })} + + ) +} diff --git a/apps/cli/src/ui/components/Select.tsx b/apps/cli/src/ui/components/Select.tsx new file mode 100644 index 00000000000..18b43a29332 --- /dev/null +++ b/apps/cli/src/ui/components/Select.tsx @@ -0,0 +1,38 @@ +import { colors } from '@universe/cli/src/ui/utils/colors' +import { Text } from 'ink' +import SelectInput, { type IndicatorProps, type ItemProps } from 'ink-select-input' + +interface SelectItem { + label: string + value: string +} + +interface SelectProps { + items: SelectItem[] + onSelect: (item: SelectItem) => void +} + +/** + * Themed SelectInput wrapper with Uniswap pink highlighting + */ +export function Select({ items, onSelect }: SelectProps): JSX.Element { + // Custom item component with pink color + const itemComponent = ({ isSelected, label }: ItemProps): JSX.Element => ( + + {isSelected ? '❯ ' : ' '} + {label} + + ) + + // Empty indicator component to disable default blue chevron + const indicatorComponent = (_props: IndicatorProps): JSX.Element => <> + + return ( + + ) +} diff --git a/apps/cli/src/ui/components/StatusBadge.tsx b/apps/cli/src/ui/components/StatusBadge.tsx new file mode 100644 index 00000000000..16e501d1b2c --- /dev/null +++ b/apps/cli/src/ui/components/StatusBadge.tsx @@ -0,0 +1,20 @@ +import { colors } from '@universe/cli/src/ui/utils/colors' +import { Text } from 'ink' + +type StatusType = 'success' | 'warning' | 'error' | 'info' + +interface StatusBadgeProps { + type: StatusType + children: React.ReactNode +} + +const statusColors: Record = { + success: colors.success, + warning: colors.warning, + error: colors.error, + info: colors.primary, +} + +export function StatusBadge({ type, children }: StatusBadgeProps): JSX.Element { + return {children} +} diff --git a/apps/cli/src/ui/components/TextInput.tsx b/apps/cli/src/ui/components/TextInput.tsx new file mode 100644 index 00000000000..c26dd66cdab --- /dev/null +++ b/apps/cli/src/ui/components/TextInput.tsx @@ -0,0 +1,38 @@ +import { colors } from '@universe/cli/src/ui/utils/colors' +import { Box, Text } from 'ink' + +interface TextInputProps { + label: string + value: string + onChange: (value: string) => void + focused?: boolean + isEditing?: boolean + editValue?: string + placeholder?: string +} + +export function TextInput({ + label, + value, + onChange: _onChange, + focused = false, + isEditing = false, + editValue, + placeholder = '', +}: TextInputProps): JSX.Element { + const displayValue = isEditing && editValue !== undefined ? editValue : value || placeholder + + return ( + + + {focused ? '❯ ' : ' '} + {label}:{' '} + + {displayValue} + + {isEditing && (Enter to save, Esc to cancel, type text)} + {focused && !isEditing && (Enter to edit)} + + + ) +} diff --git a/apps/cli/src/ui/components/Toggle.tsx b/apps/cli/src/ui/components/Toggle.tsx new file mode 100644 index 00000000000..38b1e6cf106 --- /dev/null +++ b/apps/cli/src/ui/components/Toggle.tsx @@ -0,0 +1,22 @@ +import { colors } from '@universe/cli/src/ui/utils/colors' +import { Text } from 'ink' + +interface ToggleProps { + label: string + checked: boolean + onToggle: () => void + focused?: boolean +} + +/** + * Toggle component - does not handle its own input + * Parent component should handle Enter/Space when this is focused + */ +export function Toggle({ label, checked, onToggle: _onToggle, focused = false }: ToggleProps): JSX.Element { + return ( + + {focused ? '❯ ' : ' '} + {checked ? '◉' : '○'} {label} + + ) +} diff --git a/apps/cli/src/ui/components/WindowedSelect.tsx b/apps/cli/src/ui/components/WindowedSelect.tsx new file mode 100644 index 00000000000..13d514398cc --- /dev/null +++ b/apps/cli/src/ui/components/WindowedSelect.tsx @@ -0,0 +1,101 @@ +import { colors } from '@universe/cli/src/ui/utils/colors' +import { Box, Text, useInput } from 'ink' +import { useCallback, useEffect, useState } from 'react' + +interface WindowedSelectItem { + label: string + value: string + data?: T +} + +interface WindowedSelectProps { + items: WindowedSelectItem[] + onSelect: (item: WindowedSelectItem) => void + onFocusChange?: (item: WindowedSelectItem | null) => void + limit?: number // Number of visible items (default: 10) +} + +const DEFAULT_LIMIT = 10 + +export function WindowedSelect({ + items, + onSelect, + onFocusChange, + limit = DEFAULT_LIMIT, +}: WindowedSelectProps): JSX.Element { + const [selectedIndex, setSelectedIndex] = useState(0) + const [startIndex, setStartIndex] = useState(0) + + // Notify parent when focused item changes + useEffect(() => { + if (onFocusChange) { + const focusedItem = items[selectedIndex] ?? null + onFocusChange(focusedItem) + } + }, [selectedIndex, items, onFocusChange]) + + // Calculate visible window + const endIndex = Math.min(startIndex + limit, items.length) + const visibleItems = items.slice(startIndex, endIndex) + const relativeSelectedIndex = selectedIndex - startIndex + + // Keep selected item in view when it moves outside the window + useEffect(() => { + if (selectedIndex < startIndex) { + // Selected item moved above visible window + setStartIndex(Math.max(0, selectedIndex)) + } else if (selectedIndex >= endIndex) { + // Selected item moved below visible window + setStartIndex(Math.max(0, selectedIndex - limit + 1)) + } + }, [selectedIndex, startIndex, endIndex, limit]) + + // Reset to top when items change + useEffect(() => { + setSelectedIndex(0) + setStartIndex(0) + }, []) + + // Handle keyboard input + useInput( + useCallback( + (input: string, key: { upArrow?: boolean; downArrow?: boolean; return?: boolean }) => { + if (key.upArrow && selectedIndex > 0) { + setSelectedIndex(selectedIndex - 1) + } else if (key.downArrow && selectedIndex < items.length - 1) { + setSelectedIndex(selectedIndex + 1) + } else if (key.return) { + const selectedItem = items[selectedIndex] + if (selectedItem) { + onSelect(selectedItem) + } + } + }, + [selectedIndex, items, onSelect], + ), + ) + + const hasMoreAbove = startIndex > 0 + const hasMoreBelow = endIndex < items.length + + return ( + + {hasMoreAbove && ... {startIndex} more above (use ↑ to scroll) ...} + {visibleItems.map((item, index) => { + const isSelected = index === relativeSelectedIndex + return ( + + {isSelected ? '❯ ' : ' '} + {item.label} + + ) + })} + {hasMoreBelow && ... {items.length - endIndex} more below (use ↓ to scroll) ...} + + + Selected: {selectedIndex + 1} of {items.length} (Enter to select) + + + + ) +} diff --git a/apps/cli/src/ui/hooks/useAnalysis.ts b/apps/cli/src/ui/hooks/useAnalysis.ts new file mode 100644 index 00000000000..e66406d0991 --- /dev/null +++ b/apps/cli/src/ui/hooks/useAnalysis.ts @@ -0,0 +1,53 @@ +import type { OrchestratorConfig } from '@universe/cli/src/core/orchestrator' +import { OrchestratorService, type ProgressEvent } from '@universe/cli/src/ui/services/orchestrator-service' +import { useCallback, useState } from 'react' + +interface UseAnalysisResult { + execute: (config: OrchestratorConfig) => Promise | null> + results: Record | null + progress: ProgressEvent | null + error: Error | null + isRunning: boolean +} + +export function useAnalysis(): UseAnalysisResult { + const [results, setResults] = useState | null>(null) + const [progress, setProgress] = useState(null) + const [error, setError] = useState(null) + const [isRunning, setIsRunning] = useState(false) + const [service] = useState(() => new OrchestratorService()) + + const execute = useCallback( + async (config: OrchestratorConfig): Promise | null> => { + try { + setIsRunning(true) + setError(null) + setProgress({ stage: 'idle' }) + setResults(null) + + const result = await service.execute(config, (event: ProgressEvent) => { + setProgress(event) + }) + + setResults(result) + setIsRunning(false) + return result + } catch (err) { + const errorObj = err instanceof Error ? err : new Error(String(err)) + setError(errorObj) + setIsRunning(false) + setProgress({ stage: 'error', message: errorObj.message }) + return null + } + }, + [service], + ) + + return { + execute, + results, + progress, + error, + isRunning, + } +} diff --git a/apps/cli/src/ui/hooks/useAppState.tsx b/apps/cli/src/ui/hooks/useAppState.tsx new file mode 100644 index 00000000000..f786dc9faed --- /dev/null +++ b/apps/cli/src/ui/hooks/useAppState.tsx @@ -0,0 +1,132 @@ +import type { OrchestratorConfig } from '@universe/cli/src/core/orchestrator' +import type { Release } from '@universe/cli/src/lib/release-scanner' +import { createContext, type ReactNode, useContext, useReducer } from 'react' + +export type Screen = + | 'welcome' + | 'release-select' + | 'team-select' + | 'config-review' + | 'execution' + | 'results' + | 'bug-input' + +export type AnalysisMode = 'release-changelog' | 'team-digest' | 'changelog' | 'bug-bisect' + +export interface TeamFilter { + teams?: string[] + usernames?: string[] + emails?: string[] +} + +export interface TeamMembersCache { + emails: string[] + usernames: string[] +} + +interface AppState { + screen: Screen + repository: { owner: string; name: string } | null + releases: Release[] + selectedRelease: Release | null + comparisonRelease: Release | null + analysisMode: AnalysisMode + bugDescription: string | null + teamFilter: TeamFilter | null + teamMembersCache: Record + timePeriod: string + config: Partial + executionState: 'idle' | 'running' | 'complete' | 'error' + results: { changelog: string; metadata: unknown } | null +} + +type AppAction = + | { type: 'SET_SCREEN'; screen: Screen } + | { type: 'SET_REPOSITORY'; repository: { owner: string; name: string } | null } + | { type: 'SET_RELEASES'; releases: Release[] } + | { type: 'SELECT_RELEASE'; release: Release | null } + | { type: 'SET_COMPARISON_RELEASE'; release: Release | null } + | { type: 'SET_ANALYSIS_MODE'; mode: AnalysisMode } + | { type: 'SET_BUG_DESCRIPTION'; description: string | null } + | { type: 'SET_TEAM_FILTER'; filter: TeamFilter | null } + | { type: 'CACHE_TEAM_MEMBERS'; teamSlug: string; members: TeamMembersCache } + | { type: 'SET_TIME_PERIOD'; period: string } + | { type: 'UPDATE_CONFIG'; config: Partial } + | { type: 'SET_EXECUTION_STATE'; state: 'idle' | 'running' | 'complete' | 'error' } + | { type: 'SET_RESULTS'; results: { changelog: string; metadata: unknown } | null } + +const initialState: AppState = { + screen: 'welcome', + repository: null, + releases: [], + selectedRelease: null, + comparisonRelease: null, + analysisMode: 'release-changelog', + bugDescription: null, + teamFilter: null, + teamMembersCache: {}, + timePeriod: '30 days ago', + config: {}, + executionState: 'idle', + results: null, +} + +function appReducer(state: AppState, action: AppAction): AppState { + switch (action.type) { + case 'SET_SCREEN': + return { ...state, screen: action.screen } + case 'SET_REPOSITORY': + return { ...state, repository: action.repository } + case 'SET_RELEASES': + return { ...state, releases: action.releases } + case 'SELECT_RELEASE': + return { ...state, selectedRelease: action.release } + case 'SET_COMPARISON_RELEASE': + return { ...state, comparisonRelease: action.release } + case 'SET_ANALYSIS_MODE': + return { ...state, analysisMode: action.mode } + case 'SET_BUG_DESCRIPTION': + return { ...state, bugDescription: action.description } + case 'SET_TEAM_FILTER': + return { ...state, teamFilter: action.filter } + case 'CACHE_TEAM_MEMBERS': + return { + ...state, + teamMembersCache: { + ...state.teamMembersCache, + [action.teamSlug]: action.members, + }, + } + case 'SET_TIME_PERIOD': + return { ...state, timePeriod: action.period } + case 'UPDATE_CONFIG': + return { ...state, config: { ...state.config, ...action.config } } + case 'SET_EXECUTION_STATE': + return { ...state, executionState: action.state } + case 'SET_RESULTS': + return { ...state, results: action.results } + default: + return state + } +} + +interface AppContextValue { + state: AppState + dispatch: React.Dispatch +} + +const AppContext = createContext(null) + +export function AppStateProvider({ children }: { children: ReactNode }): JSX.Element { + const [state, dispatch] = useReducer(appReducer, initialState) + + return {children} +} + +export function useAppState(): AppContextValue { + const context = useContext(AppContext) + if (!context) { + throw new Error('useAppState must be used within AppStateProvider') + } + return context +} diff --git a/apps/cli/src/ui/hooks/useEditableField.ts b/apps/cli/src/ui/hooks/useEditableField.ts new file mode 100644 index 00000000000..4f407921586 --- /dev/null +++ b/apps/cli/src/ui/hooks/useEditableField.ts @@ -0,0 +1,107 @@ +import { useCallback, useEffect, useState } from 'react' + +interface UseEditableFieldOptions { + value: T + onChange: (value: T) => void + focused: boolean + type?: 'text' | 'number' + min?: number + max?: number + step?: number + onEditStart?: () => void + onEditEnd?: () => void +} + +interface UseEditableFieldReturn { + isEditing: boolean + editValue: string + startEdit: () => void + saveEdit: () => void + cancelEdit: () => void + handleInput: (input: string, key: { backspace?: boolean; delete?: boolean }) => void +} + +/** + * Hook for managing editable field state and keyboard input + * Handles edit mode (Enter to edit, Esc to cancel, typing) + */ +export function useEditableField({ + value, + onChange, + focused, + type = 'text', + min, + max, + step: _step = 1, + onEditStart, + onEditEnd, +}: UseEditableFieldOptions): UseEditableFieldReturn { + const [isEditing, setIsEditing] = useState(false) + const [editValue, setEditValue] = useState('') + + const startEdit = useCallback(() => { + setIsEditing(true) + setEditValue(String(value)) + onEditStart?.() + }, [value, onEditStart]) + + const saveEdit = useCallback(() => { + if (type === 'number') { + const numValue = Number.parseInt(editValue, 10) + if (!Number.isNaN(numValue)) { + const clampedValue = Math.max(min ?? 0, Math.min(max ?? Number.MAX_SAFE_INTEGER, numValue)) + onChange(clampedValue as T) + } + } else { + onChange(editValue as T) + } + setIsEditing(false) + setEditValue('') + onEditEnd?.() + }, [editValue, type, min, max, onChange, onEditEnd]) + + const cancelEdit = useCallback(() => { + setIsEditing(false) + setEditValue('') + onEditEnd?.() + }, [onEditEnd]) + + // Reset editing state when focus is lost + useEffect(() => { + if (!focused && isEditing) { + cancelEdit() + } + }, [focused, isEditing, cancelEdit]) + + const handleInput = useCallback( + (input: string, key: { backspace?: boolean; delete?: boolean; return?: boolean; escape?: boolean }) => { + if (key.return) { + if (isEditing) { + saveEdit() + } else { + startEdit() + } + } else if (key.escape && isEditing) { + cancelEdit() + } else if (key.backspace || key.delete) { + setEditValue((prev) => prev.slice(0, -1)) + } else if (input && input.length === 1) { + if (type === 'number' && /^\d$/.test(input)) { + setEditValue((prev) => prev + input) + } else if (type === 'text') { + setEditValue((prev) => prev + input) + } + } + }, + [type, isEditing, saveEdit, cancelEdit, startEdit], + ) + + return { + isEditing, + editValue, + startEdit, + saveEdit, + cancelEdit, + handleInput, + } +} diff --git a/apps/cli/src/ui/hooks/useFormNavigation.ts b/apps/cli/src/ui/hooks/useFormNavigation.ts new file mode 100644 index 00000000000..125cd62cb9c --- /dev/null +++ b/apps/cli/src/ui/hooks/useFormNavigation.ts @@ -0,0 +1,67 @@ +import { useInput } from 'ink' +import { useCallback, useState } from 'react' + +interface UseFormNavigationOptions { + itemCount: number + onEscape?: () => void + enabled?: boolean + // Optional: block navigation when true (e.g., when editing a field) + blockNavigation?: boolean +} + +interface UseFormNavigationReturn { + focusedIndex: number + setFocusedIndex: (index: number) => void +} + +/** + * Hook for managing keyboard navigation in forms + * Handles up/down arrow navigation only - selection logic handled by parent + */ +export function useFormNavigation({ + itemCount, + onEscape, + enabled = true, + blockNavigation = false, +}: UseFormNavigationOptions): UseFormNavigationReturn { + const [focusedIndex, setFocusedIndex] = useState(0) + + const arrowUp = useCallback(() => { + setFocusedIndex((prev) => Math.max(0, prev - 1)) + }, []) + + const arrowDown = useCallback(() => { + setFocusedIndex((prev) => Math.min(itemCount - 1, prev + 1)) + }, [itemCount]) + + const handleEscape = useCallback(() => { + if (onEscape) { + onEscape() + } + }, [onEscape]) + + // Register keyboard handlers - only handles navigation arrows + useInput( + useCallback( + (_input: string, key: { upArrow?: boolean; downArrow?: boolean; escape?: boolean }) => { + if (!enabled || blockNavigation) { + return + } + + if (key.upArrow) { + arrowUp() + } else if (key.downArrow) { + arrowDown() + } else if (key.escape && onEscape) { + handleEscape() + } + }, + [enabled, blockNavigation, arrowUp, arrowDown, handleEscape, onEscape], + ), + ) + + return { + focusedIndex, + setFocusedIndex, + } +} diff --git a/apps/cli/src/ui/hooks/useReleases.ts b/apps/cli/src/ui/hooks/useReleases.ts new file mode 100644 index 00000000000..55eb85a0484 --- /dev/null +++ b/apps/cli/src/ui/hooks/useReleases.ts @@ -0,0 +1,106 @@ +import { type Release, ReleaseScanner } from '@universe/cli/src/lib/release-scanner' +import { useCallback, useEffect, useRef, useState } from 'react' + +interface UseReleasesResult { + releases: Release[] + loading: boolean + error: Error | null + getLatest: (platform: 'mobile' | 'extension') => Promise + getPrevious: (release: Release) => Promise + findRelease: (platform: 'mobile' | 'extension', version: string) => Promise + refresh: (platform?: 'mobile' | 'extension') => Promise +} + +export function useReleases(platform?: 'mobile' | 'extension'): UseReleasesResult { + const [releases, setReleases] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [scanner] = useState(() => new ReleaseScanner()) + const isScanningRef = useRef(false) + const lastScannedPlatformRef = useRef(undefined) + + const refresh = useCallback( + async (filterPlatform?: 'mobile' | 'extension') => { + // Guard: Prevent multiple simultaneous scans + if (isScanningRef.current) { + return + } + + const targetPlatform = filterPlatform || platform + const platformKey = targetPlatform || 'all' + + // Guard: Don't re-scan if we already scanned for this platform + if (lastScannedPlatformRef.current === platformKey) { + return + } + + try { + isScanningRef.current = true + setLoading(true) + setError(null) + const fetched = await scanner.scanReleases(targetPlatform) + setReleases(fetched) + lastScannedPlatformRef.current = platformKey + setLoading(false) + } catch (err) { + setError(err instanceof Error ? err : new Error(String(err))) + setLoading(false) + } finally { + isScanningRef.current = false + } + }, + [scanner, platform], + ) + + useEffect(() => { + // Only scan on initial mount or when platform actually changes + const platformKey = platform || 'all' + if (lastScannedPlatformRef.current !== platformKey) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises -- Intentionally fire-and-forget promise + refresh() + } + }, [platform, refresh]) + + const getLatest = useCallback( + async (targetPlatform: 'mobile' | 'extension'): Promise => { + try { + return await scanner.getLatestRelease(targetPlatform) + } catch (_err) { + return null + } + }, + [scanner], + ) + + const getPrevious = useCallback( + async (release: Release): Promise => { + try { + return await scanner.getPreviousRelease(release) + } catch (_err) { + return null + } + }, + [scanner], + ) + + const findRelease = useCallback( + async (targetPlatform: 'mobile' | 'extension', version: string): Promise => { + try { + return await scanner.findRelease(targetPlatform, version) + } catch (_err) { + return null + } + }, + [scanner], + ) + + return { + releases, + loading, + error, + getLatest, + getPrevious, + findRelease, + refresh, + } +} diff --git a/apps/cli/src/ui/hooks/useRepository.ts b/apps/cli/src/ui/hooks/useRepository.ts new file mode 100644 index 00000000000..ca3ed53b81a --- /dev/null +++ b/apps/cli/src/ui/hooks/useRepository.ts @@ -0,0 +1,53 @@ +import { detectRepository } from '@universe/cli/src/lib/team-resolver' +import { useEffect, useState } from 'react' + +interface Repository { + owner: string + name: string +} + +interface UseRepositoryResult { + repository: Repository | null + loading: boolean + error: Error | null +} + +export function useRepository(): UseRepositoryResult { + const [repository, setRepository] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + let cancelled = false + + async function detect(): Promise { + try { + setLoading(true) + setError(null) + const detected = await detectRepository() + if (!cancelled) { + if (detected && detected.owner && detected.name) { + setRepository({ owner: detected.owner, name: detected.name }) + } else { + setRepository(null) + } + setLoading(false) + } + } catch (err) { + if (!cancelled) { + setError(err instanceof Error ? err : new Error(String(err))) + setLoading(false) + } + } + } + + // eslint-disable-next-line @typescript-eslint/no-floating-promises -- Intentionally fire-and-forget promise + detect() + + return () => { + cancelled = true + } + }, []) + + return { repository, loading, error } +} diff --git a/apps/cli/src/ui/hooks/useTeams.ts b/apps/cli/src/ui/hooks/useTeams.ts new file mode 100644 index 00000000000..71c7e59bd82 --- /dev/null +++ b/apps/cli/src/ui/hooks/useTeams.ts @@ -0,0 +1,94 @@ +import { $ } from 'bun' +import { useCallback, useEffect, useRef, useState } from 'react' + +export interface GitHubTeam { + name: string + slug: string + description: string | null + membersCount?: number +} + +interface UseTeamsResult { + teams: GitHubTeam[] + loading: boolean + error: Error | null + refresh: () => Promise +} + +/** + * Hook to fetch teams from a GitHub organization + */ +export function useTeams(org: string | null): UseTeamsResult { + const [teams, setTeams] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const isFetchingRef = useRef(false) + const lastFetchedOrgRef = useRef(null) + + const refresh = useCallback(async () => { + if (!org) { + setLoading(false) + return + } + + // Guard: Prevent multiple simultaneous fetches + if (isFetchingRef.current) { + return + } + + // Guard: Don't re-fetch if we already fetched for this org + if (lastFetchedOrgRef.current === org) { + return + } + + try { + isFetchingRef.current = true + setLoading(true) + setError(null) + + // Fetch teams from GitHub API + const teamsResult = + await $`gh api /orgs/${org}/teams --jq '.[] | {name: .name, slug: .slug, description: .description}'`.text() + + const teamLines = teamsResult.trim().split('\n').filter(Boolean) + const parsedTeams: GitHubTeam[] = teamLines + .map((line: string) => { + try { + const parsed = JSON.parse(line) as GitHubTeam + return parsed + } catch { + return null + } + }) + .filter((team: GitHubTeam | null): team is GitHubTeam => team !== null) + + setTeams(parsedTeams) + lastFetchedOrgRef.current = org + setLoading(false) + } catch (err) { + setError( + err instanceof Error + ? err + : new Error(`Failed to fetch teams from org "${org}". Ensure gh CLI is authenticated.`), + ) + setLoading(false) + } finally { + isFetchingRef.current = false + } + }, [org]) + + useEffect(() => { + // Only fetch on initial mount or when org changes + if (org && lastFetchedOrgRef.current !== org) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises -- Intentionally fire-and-forget promise + refresh() + } + }, [org, refresh]) + + return { + teams, + loading, + error, + refresh, + } +} diff --git a/apps/cli/src/ui/hooks/useToggleGroup.ts b/apps/cli/src/ui/hooks/useToggleGroup.ts new file mode 100644 index 00000000000..5be9a703328 --- /dev/null +++ b/apps/cli/src/ui/hooks/useToggleGroup.ts @@ -0,0 +1,70 @@ +import { useCallback, useState } from 'react' + +interface UseToggleGroupOptions { + items: Array<{ key: T; label: string }> + initialSelected?: Set + minSelection?: number // Minimum number of items that must be selected +} + +interface UseToggleGroupReturn { + selected: Set + toggle: (key: T) => void + isSelected: (key: T) => boolean + selectAll: () => void + deselectAll: () => void +} + +/** + * Hook for managing a group of toggles/checkboxes + * Useful for multiple selection scenarios like output options + */ +export function useToggleGroup({ + items, + initialSelected = new Set(), + minSelection = 0, +}: UseToggleGroupOptions): UseToggleGroupReturn { + const [selected, setSelected] = useState>(initialSelected) + + const toggle = useCallback( + (key: T) => { + setSelected((prev) => { + const next = new Set(prev) + if (next.has(key)) { + // Don't allow unchecking if it would violate minSelection + if (next.size > minSelection) { + next.delete(key) + } + } else { + next.add(key) + } + return next + }) + }, + [minSelection], + ) + + const isSelected = useCallback( + (key: T) => { + return selected.has(key) + }, + [selected], + ) + + const selectAll = useCallback(() => { + setSelected(new Set(items.map((item) => item.key))) + }, [items]) + + const deselectAll = useCallback(() => { + if (items.length >= minSelection) { + setSelected(new Set(items.slice(0, minSelection).map((item) => item.key))) + } + }, [items, minSelection]) + + return { + selected, + toggle, + isSelected, + selectAll, + deselectAll, + } +} diff --git a/apps/cli/src/ui/screens/BugBisectResultsScreen.tsx b/apps/cli/src/ui/screens/BugBisectResultsScreen.tsx new file mode 100644 index 00000000000..69b065a0e62 --- /dev/null +++ b/apps/cli/src/ui/screens/BugBisectResultsScreen.tsx @@ -0,0 +1,464 @@ +import { join } from 'node:path' +import { Select } from '@universe/cli/src/ui/components/Select' +import { TextInput } from '@universe/cli/src/ui/components/TextInput' +import { useAppState } from '@universe/cli/src/ui/hooks/useAppState' +import { useRepository } from '@universe/cli/src/ui/hooks/useRepository' +import { colors } from '@universe/cli/src/ui/utils/colors' +import { Box as InkBox, Text, useInput } from 'ink' +import { useCallback, useMemo, useState } from 'react' + +interface BugBisectResultsScreenProps { + results: { changelog: string; metadata: unknown } + onRestart: () => void +} + +interface SuspiciousCommit { + sha: string + confidence: number + reasoning: string + relatedPR?: number +} + +interface BugBisectResults { + suspiciousCommits?: SuspiciousCommit[] + summary?: string + totalCommitsAnalyzed?: number + releaseContext?: { + from: string + to: string + platform: string + } +} + +type ViewMode = 'menu' | 'save-file' | 'saved' + +function isValidBugBisectResults(value: unknown): value is BugBisectResults { + return ( + typeof value === 'object' && + value !== null && + 'suspiciousCommits' in value && + Array.isArray((value as BugBisectResults).suspiciousCommits) + ) +} + +function tryParseFromString(jsonString: string): BugBisectResults | null { + try { + const parsed = JSON.parse(jsonString) as BugBisectResults + if (isValidBugBisectResults(parsed)) { + return parsed + } + } catch { + // Not valid JSON or not valid BugBisectResults + } + return null +} + +function getConfidenceColor(confidence: number): string { + if (confidence >= 0.9) { + return 'red' + } + if (confidence >= 0.7) { + return '#ff8c00' + } // orange + if (confidence >= 0.5) { + return 'yellow' + } + return 'gray' +} + +function getConfidenceLabel(confidence: number): string { + if (confidence >= 0.9) { + return 'Very Likely' + } + if (confidence >= 0.7) { + return 'Likely' + } + if (confidence >= 0.5) { + return 'Possible' + } + return 'Unlikely' +} + +export function BugBisectResultsScreen({ results, onRestart }: BugBisectResultsScreenProps): JSX.Element { + const { state } = useAppState() + const { repository } = useRepository() + const [viewMode, setViewMode] = useState('menu') + const [filename, setFilename] = useState('bug-bisect-results.json') + const [filepath, setFilepath] = useState(process.cwd()) + const [savedPath, setSavedPath] = useState('') + const [focusedIndex, setFocusedIndex] = useState(0) + const [editingIndex, setEditingIndex] = useState(null) + const [editValue, setEditValue] = useState('') + const [saveError, setSaveError] = useState(null) + + // Parse results + const parsedResults = useMemo((): BugBisectResults | null => { + try { + // Try to parse from metadata if it's already parsed + if (results.metadata && typeof results.metadata === 'object') { + const metadata = results.metadata as Record + if (isValidBugBisectResults(metadata)) { + return metadata + } + } + + // Try to parse from changelog string (might be JSON) + if (typeof results.changelog === 'string') { + const parsed = tryParseFromString(results.changelog) + if (parsed) { + return parsed + } + } + + // Try to parse from metadata.analysis if it's a string + if (results.metadata && typeof results.metadata === 'object') { + const metadata = results.metadata as Record + const analysisString = metadata.analysis + if (typeof analysisString === 'string') { + const parsed = tryParseFromString(analysisString) + if (parsed) { + return parsed + } + } + } + + return null + } catch { + return null + } + }, [results]) + + const bugResults = useMemo((): BugBisectResults => { + if (parsedResults) { + return parsedResults + } + return { + suspiciousCommits: [], + summary: 'Failed to parse results', + totalCommitsAnalyzed: 0, + releaseContext: state.selectedRelease + ? { + from: state.comparisonRelease?.version || 'unknown', + to: state.selectedRelease.version, + platform: state.selectedRelease.platform, + } + : undefined, + } + }, [parsedResults, state.selectedRelease, state.comparisonRelease]) + + const githubBaseUrl = repository ? `https://github.com/${repository.owner}/${repository.name}` : '' + + const options = [ + { label: 'Save to File', value: 'save' }, + { label: 'Start Over', value: 'restart' }, + { label: 'Quit', value: 'quit' }, + ] + + const handleSelect = (option: { label: string; value: string }): void => { + if (option.value === 'quit') { + process.exit(0) + } else if (option.value === 'restart') { + onRestart() + } else if (option.value === 'save') { + setViewMode('save-file') + } + } + + const saveFile = useCallback(async () => { + try { + const fullPath = join(filepath, filename) + const content = JSON.stringify(bugResults, null, 2) + await Bun.write(fullPath, content) + setSavedPath(fullPath) + setViewMode('saved') + setSaveError(null) + } catch (error) { + setSaveError(error instanceof Error ? error.message : 'Failed to save file') + } + }, [filepath, filename, bugResults]) + + // Handle input for save-file mode (similar to ResultsScreen) + useInput( + useCallback( + (input, key) => { + if (viewMode !== 'save-file') { + return + } + + const isEditing = editingIndex !== null + + if (key.escape) { + if (isEditing) { + setEditingIndex(null) + setEditValue('') + } else { + setViewMode('menu') + setFocusedIndex(0) + setSaveError(null) + } + return + } + + if (!isEditing) { + if (key.upArrow) { + setFocusedIndex((prev) => Math.max(0, prev - 1)) + return + } + if (key.downArrow) { + setFocusedIndex((prev) => Math.min(2, prev + 1)) + return + } + if (key.return) { + if (focusedIndex === 0 || focusedIndex === 1) { + setEditingIndex(focusedIndex) + setEditValue(focusedIndex === 0 ? filename : filepath) + } else if (focusedIndex === 2) { + saveFile().catch(() => { + // Error already handled in saveFile + }) + } + return + } + } + + if (isEditing) { + if (key.return) { + if (editingIndex === 0) { + setFilename(editValue) + } else if (editingIndex === 1) { + setFilepath(editValue) + } + setEditingIndex(null) + setEditValue('') + return + } + + if (key.backspace || key.delete) { + setEditValue((prev) => prev.slice(0, -1)) + return + } + + if (input && input.length === 1) { + setEditValue((prev) => prev + input) + return + } + } + }, + [viewMode, focusedIndex, editingIndex, editValue, filename, filepath, saveFile], + ), + ) + + // Handle input for saved mode + useInput( + useCallback( + (_input, key) => { + if (viewMode === 'saved' && (key.return || key.escape)) { + setViewMode('menu') + setFocusedIndex(0) + } + }, + [viewMode], + ), + ) + + if (viewMode === 'save-file') { + return ( + + + + Save Results to File + + + + + + + + + + {focusedIndex === 2 ? '❯ ' : ' '} + Save File + + + + {saveError && ( + + Error: {saveError} + + )} + + + + Use ↑↓ to navigate, Enter to edit/save, Esc to {editingIndex !== null ? 'cancel' : 'go back'} + + + + + ) + } + + if (viewMode === 'saved') { + return ( + + + + ✓ File Saved Successfully + + + + + + Saved to:{' '} + + {savedPath} + + + + + Press Enter or Esc to return to menu + + + + ) + } + + const suspiciousCommits = bugResults.suspiciousCommits || [] + + return ( + + + + ✓ Bug Analysis Complete + + + + + {/* Bug Description */} + {state.bugDescription && ( + + Bug Description + {state.bugDescription} + + )} + + {/* Release Context */} + {bugResults.releaseContext && ( + + + Platform: {bugResults.releaseContext.platform} + + + Release: {bugResults.releaseContext.from} → {bugResults.releaseContext.to} + + {bugResults.totalCommitsAnalyzed !== undefined && ( + + Commits Analyzed: {bugResults.totalCommitsAnalyzed} + + )} + + )} + + {/* Summary */} + {bugResults.summary && ( + + {bugResults.summary} + + )} + + {/* Suspicious Commits */} + {suspiciousCommits.length > 0 ? ( + + + + Suspicious Commits ({suspiciousCommits.length}) + + + + {suspiciousCommits.slice(0, 20).map((commit, index) => { + const confidenceColor = getConfidenceColor(commit.confidence) + const confidenceLabel = getConfidenceLabel(commit.confidence) + const shortSha = commit.sha.slice(0, 7) + const commitUrl = githubBaseUrl ? `${githubBaseUrl}/commit/${commit.sha}` : '' + const prUrl = commit.relatedPR && githubBaseUrl ? `${githubBaseUrl}/pull/${commit.relatedPR}` : '' + + return ( + + + + #{index + 1}. {shortSha} + + + + {confidenceLabel} ({Math.round(commit.confidence * 100)}%) + + + + + + {commitUrl ? ( + + {commitUrl} + + ) : ( + SHA: {commit.sha} + )} + + {commit.relatedPR && ( + + PR #{commit.relatedPR} + {prUrl && ( + + {' '} + - {prUrl} + + )} + + )} + + + + {commit.reasoning} + + + ) + })} + + {suspiciousCommits.length > 20 && ( + + ... and {suspiciousCommits.length - 20} more commits + + )} + + ) : ( + + ⚠ No suspicious commits found + + )} + + + + What would you like to do next? + + + + { + if (item.value === 'back') { + setMode('browse') + } else { + setPlatformFilter(item.value as 'mobile' | 'extension' | 'all') + setMode('browse') + } + }} + /> + + + ) + } + + // Browse mode - show releases + if (mode === 'browse') { + // Create options with navigation and filter controls at the top + const browseOptions = [ + { label: '← Back to Quick Actions', value: 'back' }, + { label: '🔍 Filter by Platform', value: 'filter' }, + ...filteredReleases.map((release: Release, index: number) => ({ + label: `${release.platform}/${release.version} (${formatBranch(release.branch)})`, + value: String(index + 2), // Offset by 2 for back and filter options + release, + })), + ] + + return ( + + + + Select Release + + + + {loading && ( + + Loading releases... + + )} + {error && ( + + Error: {error.message} + + )} + + {!loading && !error && ( + <> + + Showing {filteredReleases.length} release{filteredReleases.length !== 1 ? 's' : ''}{' '} + {platformFilter !== 'all' ? `(${platformFilter})` : ''} + + { + if (item.value === 'back') { + setMode('quick') + } else if (item.value === 'filter') { + setMode('filter-platform') + } else if (item.release) { + const release = item.release + const index = filteredReleases.findIndex( + (r: Release) => r.platform === release.platform && r.version === release.version, + ) + if (index >= 0) { + handleBrowseSelect(index) + } + } + }} + /> + + )} + + + ) + } + + return ( + + + + Release Selection + + + + + Choose a quick action or browse releases: + + + + + ) +} diff --git a/apps/cli/src/ui/screens/TeamDetailsScreen.tsx b/apps/cli/src/ui/screens/TeamDetailsScreen.tsx new file mode 100644 index 00000000000..55086f74b85 --- /dev/null +++ b/apps/cli/src/ui/screens/TeamDetailsScreen.tsx @@ -0,0 +1,152 @@ +import { fetchTeamMembers, type TeamMember } from '@universe/cli/src/lib/team-members' +import { resolveTeam } from '@universe/cli/src/lib/team-resolver' +import { Select } from '@universe/cli/src/ui/components/Select' +import { StatusBadge } from '@universe/cli/src/ui/components/StatusBadge' +import { type TeamFilter, useAppState } from '@universe/cli/src/ui/hooks/useAppState' +import type { GitHubTeam } from '@universe/cli/src/ui/hooks/useTeams' +import { colors } from '@universe/cli/src/ui/utils/colors' +import { Box, Text } from 'ink' +import Spinner from 'ink-spinner' +import { useEffect, useState } from 'react' + +interface TeamDetailsScreenProps { + team: GitHubTeam + org: string + onSelect: (teamFilter: TeamFilter) => void + onBack: () => void +} + +export function TeamDetailsScreen({ team, org, onSelect, onBack }: TeamDetailsScreenProps): JSX.Element { + const { dispatch } = useAppState() + const [members, setMembers] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + let cancelled = false + + const loadMembers = async (): Promise => { + try { + setLoading(true) + setError(null) + + // Fetch members for display + const fetchedMembers = await fetchTeamMembers(org, team.slug) + + if (!cancelled) { + setMembers(fetchedMembers) + + // Also resolve to emails/usernames and cache for later use + const teamSlug = `@${org}/${team.slug}` + try { + const { emails, usernames } = await resolveTeam(teamSlug) + dispatch({ + type: 'CACHE_TEAM_MEMBERS', + teamSlug, + members: { emails, usernames }, + }) + } catch { + // If resolveTeam fails, continue with display but don't cache + // The user can still see members, just won't be cached + } + + setLoading(false) + } + } catch (err) { + if (!cancelled) { + setError(err instanceof Error ? err : new Error('Failed to fetch team members')) + setLoading(false) + } + } + } + + // eslint-disable-next-line @typescript-eslint/no-floating-promises -- Intentionally fire-and-forget promise + loadMembers() + + return () => { + cancelled = true + } + }, [org, team.slug, dispatch]) + + const handleSelectTeam = (): void => { + const teamFilter: TeamFilter = { + teams: [`@${org}/${team.slug}`], + } + dispatch({ type: 'SET_TEAM_FILTER', filter: teamFilter }) + onSelect(teamFilter) + } + + return ( + + + + Team Details + + + + + + @{org}/{team.slug} + + {team.name} + {team.description && {team.description}} + + + + + Members + + + {loading && ( + + + + + Loading members... + + )} + + {error && ( + + {error.message} + + )} + + {!loading && !error && members.length === 0 && ( + + No members found + + )} + + {!loading && !error && members.length > 0 && ( + + + {members.length} member{members.length !== 1 ? 's' : ''} + + + {members.map((member) => ( + + • {member.name ? `${member.name} (@${member.login})` : `@${member.login}`} + + ))} + + + )} + + + { + if (item.value === 'confirm') { + handleManualConfirm() + } else { + setMode('quick') + setManualStep('teams') + } + }} + /> + + + + ) + } + } + + // Team details mode + if (mode === 'details' && selectedTeamForDetails && org) { + return ( + { + setMode('browse') + setSelectedTeamForDetails(null) + }} + /> + ) + } + + // Browse mode + if (mode === 'browse') { + const browseOptions = [ + { label: '← Back to Quick Actions', value: 'back' }, + ...teams.map((team: GitHubTeam) => ({ + label: `@${org}/${team.slug} - ${team.name}${team.description ? ` (${team.description})` : ''}`, + value: team.slug, + team, + })), + ] + + return ( + + + + Select Team + + + + {loading && ( + + Loading teams from {org}... + + )} + {error && ( + + Error: {error.message} + + )} + + {!loading && !error && ( + <> + + Found {teams.length} team{teams.length !== 1 ? 's' : ''} in {org} + + {focusedTeam && Press Tab to view team members} + { + if (item.value === 'back') { + setMode('quick') + setFocusedTeam(null) + } else if (item.team) { + handleBrowseSelect(item.team) + } + }} + onFocusChange={(item: { value: string; team?: GitHubTeam } | null) => { + setFocusedTeam(item?.team ?? null) + }} + /> + + )} + + + ) + } + + // Default: Quick actions mode + return ( + + + + Team Filter Selection + + + + + Choose how to filter contributors: + + + + + ) +} diff --git a/apps/cli/src/ui/services/orchestrator-service.ts b/apps/cli/src/ui/services/orchestrator-service.ts new file mode 100644 index 00000000000..fe8f3777de2 --- /dev/null +++ b/apps/cli/src/ui/services/orchestrator-service.ts @@ -0,0 +1,68 @@ +import type { OrchestratorConfig } from '@universe/cli/src/core/orchestrator' +import { Orchestrator } from '@universe/cli/src/core/orchestrator' +import { createVercelAIProvider } from '@universe/cli/src/lib/ai-provider-vercel' +import { SqliteCacheProvider } from '@universe/cli/src/lib/cache-provider-sqlite' +import { type ProgressEvent, ProgressLogger, type ProgressStage } from '@universe/cli/src/lib/logger' + +export type { ProgressStage, ProgressEvent } + +export type ProgressCallback = (event: ProgressEvent) => void + +export class OrchestratorService { + private orchestrator: Orchestrator | null = null + private progressCallback: ProgressCallback | null = null + + async execute(config: OrchestratorConfig, onProgress?: ProgressCallback): Promise> { + this.progressCallback = onProgress || null + + // Create cache provider (unless bypassing cache) + const bypassCache = config.bypassCache || false + const cacheProvider = bypassCache ? undefined : new SqliteCacheProvider() + + // Create AI provider + const apiKey = process.env.ANTHROPIC_API_KEY + if (!apiKey) { + throw new Error('ANTHROPIC_API_KEY environment variable is required') + } + const aiProvider = createVercelAIProvider(apiKey) + + // Ensure repoPath is set in collect options + const configWithRepoPath: OrchestratorConfig = { + ...config, + collect: { + ...config.collect, + repoPath: config.collect.repoPath || process.cwd(), + }, + } + + // Create progress logger that emits events for interactive UI mode + const logger = new ProgressLogger((event: ProgressEvent) => { + this.emitProgress(event) + }, config.verbose || false) + + // Create orchestrator with progress logger + this.orchestrator = new Orchestrator({ + config: configWithRepoPath, + aiProvider, + cacheProvider, + logger, + }) + + try { + // Execute and capture the analysis results + const results = await this.orchestrator.execute() + return results + } finally { + // Close cache connection if used + if (cacheProvider) { + cacheProvider.close() + } + } + } + + private emitProgress(event: ProgressEvent): void { + if (this.progressCallback) { + this.progressCallback(event) + } + } +} diff --git a/apps/cli/src/ui/utils/colors.ts b/apps/cli/src/ui/utils/colors.ts new file mode 100644 index 00000000000..624a198f6c4 --- /dev/null +++ b/apps/cli/src/ui/utils/colors.ts @@ -0,0 +1,13 @@ +/** + * Theme color constants for Ink UI + * Using Uniswap pink color palette + */ +export const colors = { + primary: '#FC74FE', // Uniswap pink for interactive elements + success: '#00FF00', // Green for completions + warning: '#FFFF00', // Yellow for important info + error: '#FF0000', // Red for errors + muted: '#888888', // Gray for secondary text +} as const + +export type ColorName = keyof typeof colors diff --git a/apps/cli/src/ui/utils/format.ts b/apps/cli/src/ui/utils/format.ts new file mode 100644 index 00000000000..689d619a803 --- /dev/null +++ b/apps/cli/src/ui/utils/format.ts @@ -0,0 +1,18 @@ +/** + * Text formatting utilities for UI components + */ + +export function truncate(text: string, maxLength: number): string { + if (text.length <= maxLength) { + return text + } + return `${text.slice(0, maxLength - 3)}...` +} + +export function formatVersion(version: string): string { + return version +} + +export function formatBranch(branch: string): string { + return branch.replace('origin/releases/', '') +} diff --git a/apps/cli/tsconfig.json b/apps/cli/tsconfig.json new file mode 100644 index 00000000000..748b6c093fa --- /dev/null +++ b/apps/cli/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../config/tsconfig/app.json", + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.json", "src/global.d.ts"], + "exclude": ["src/**/*.spec.ts", "src/**/*.spec.tsx", "src/**/*.test.ts", "src/**/*.test.tsx"], + "compilerOptions": { + "noEmit": false, + "module": "esnext", + "moduleResolution": "bundler" + }, + "references": [] +} diff --git a/apps/cli/tsconfig.lint.json b/apps/cli/tsconfig.lint.json new file mode 100644 index 00000000000..79659c26038 --- /dev/null +++ b/apps/cli/tsconfig.lint.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "preserveSymlinks": true + }, + "include": ["**/*.ts", "**/*.tsx", "**/*.json"], + "exclude": ["node_modules"] +} diff --git a/apps/extension/.gitignore b/apps/extension/.gitignore index d96df7d9000..57bab834221 100644 --- a/apps/extension/.gitignore +++ b/apps/extension/.gitignore @@ -35,3 +35,5 @@ tsconfig.tsbuildinfo # E2E test artifacts e2e/test-results/ + +coverage/ diff --git a/apps/extension/jest-setup.js b/apps/extension/jest-setup.js index fcf81fe78e0..beb80a32847 100644 --- a/apps/extension/jest-setup.js +++ b/apps/extension/jest-setup.js @@ -2,6 +2,7 @@ import 'utilities/jest-package-mocks' import 'uniswap/jest-package-mocks' import 'wallet/jest-package-mocks' import 'config/jest-presets/ui/ui-package-mocks' +import 'react-native-gesture-handler/jestSetup'; import { chrome } from 'jest-chrome' import { AppearanceSettingType } from 'wallet/src/features/appearance/slice' @@ -51,6 +52,39 @@ global.chrome = { i18n: { ...global.chrome.i18n, getUILanguage: jest.fn().mockReturnValue(MOCK_LANGUAGE) + }, + storage: { + ...chrome.storage, + local: { + ...chrome.storage.local, + addListener: jest.fn(), + }, + session: { + get: jest.fn().mockImplementation((_keys, callback) => { + if (callback) { + callback({}) + } + return Promise.resolve({}) + }), + set: jest.fn().mockImplementation((_items, callback) => { + if (callback) { + callback() + } + return Promise.resolve() + }), + remove: jest.fn().mockImplementation((_keys, callback) => { + if (callback) { + callback() + } + return Promise.resolve() + }), + clear: jest.fn().mockImplementation((callback) => { + if (callback) { + callback() + } + return Promise.resolve() + }) + } } } @@ -69,10 +103,52 @@ const mockAppearanceSetting = AppearanceSettingType.System jest.mock('wallet/src/features/appearance/hooks', () => { return { useCurrentAppearanceSetting: () => mockAppearanceSetting, - } -}) -jest.mock('wallet/src/features/appearance/hooks', () => { - return { useSelectedColorScheme: () => 'light', } }) + +// Mock IntersectionObserver for Tamagui's useElementLayout +const IntersectionObserverMock = jest.fn().mockImplementation((callback) => ({ + observe: jest.fn((element) => { + // Immediately call the callback with a mock entry + if (callback && element) { + callback([ + { + target: element, + isIntersecting: true, + intersectionRatio: 1, + boundingClientRect: { + x: 0, + y: 0, + width: 100, + height: 100, + top: 0, + right: 100, + bottom: 100, + left: 0, + }, + intersectionRect: { + x: 0, + y: 0, + width: 100, + height: 100, + top: 0, + right: 100, + bottom: 100, + left: 0, + }, + rootBounds: null, + time: 0, + }, + ]) + } + }), + unobserve: jest.fn(), + disconnect: jest.fn(), + takeRecords: jest.fn().mockReturnValue([]), + root: null, + rootMargin: '', + thresholds: [], +})) + +global.IntersectionObserver = IntersectionObserverMock diff --git a/apps/extension/jest.config.js b/apps/extension/jest.config.js index 383e89187d0..cf7a5007b46 100644 --- a/apps/extension/jest.config.js +++ b/apps/extension/jest.config.js @@ -38,6 +38,10 @@ module.exports = { ], resolver: "/src/test/jest-resolver.js", displayName: 'Extension Wallet', + testMatch: [ + '/src/**/*.(spec|test).[jt]s?(x)', + '/config/**/*.(spec|test).[jt]s?(x)', + ], testPathIgnorePatterns: [ ...preset.testPathIgnorePatterns, '/e2e/', @@ -58,6 +62,5 @@ module.exports = { setupFiles: [ '../../config/jest-presets/jest/setup.js', './jest-setup.js', - '../../node_modules/react-native-gesture-handler/jestSetup.js', ], } diff --git a/apps/extension/src/app/components/AutoLockProvider.test.tsx b/apps/extension/src/app/components/AutoLockProvider.test.tsx index 0b082e9cc91..5e3f69795e7 100644 --- a/apps/extension/src/app/components/AutoLockProvider.test.tsx +++ b/apps/extension/src/app/components/AutoLockProvider.test.tsx @@ -1,252 +1,337 @@ -import { waitFor } from '@testing-library/react' import React from 'react' import { AutoLockProvider } from 'src/app/components/AutoLockProvider' import { render } from 'src/test/test-utils' import { FiatCurrency } from 'uniswap/src/features/fiatCurrency/constants' import { Language } from 'uniswap/src/features/language/constants' import { DeviceAccessTimeout } from 'uniswap/src/features/settings/constants' -import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants' -import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { logger } from 'utilities/src/logger/logger' -import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring' // Mock dependencies jest.mock('uniswap/src/extension/useIsChromeWindowFocused') -jest.mock('uniswap/src/features/telemetry/send') jest.mock('utilities/src/logger/logger') -jest.mock('wallet/src/features/wallet/Keyring/Keyring') - -// Import mocked modules with proper typing -import { useIsChromeWindowFocusedWithTimeout } from 'uniswap/src/extension/useIsChromeWindowFocused' - -const mockUseIsChromeWindowFocusedWithTimeout = useIsChromeWindowFocusedWithTimeout as jest.MockedFunction< - typeof useIsChromeWindowFocusedWithTimeout -> -const mockSendAnalyticsEvent = sendAnalyticsEvent as jest.MockedFunction -const mockLogger = logger as jest.Mocked -const mockKeyring = Keyring as jest.Mocked - -// Helper functions for common test patterns -const renderAutoLockProvider = ( - deviceAccessTimeout: DeviceAccessTimeout, - children: React.ReactNode =
Test
, -) => { - return render({children}, { - preloadedState: { - userSettings: { - currentLanguage: Language.English, - currentCurrency: FiatCurrency.UnitedStatesDollar, - hideSmallBalances: true, - hideSpamTokens: true, - hapticsEnabled: true, - deviceAccessTimeout, - }, - }, - }) +jest.mock('src/app/hooks/useIsWalletUnlocked', () => ({ + useIsWalletUnlocked: jest.fn(), + isWalletUnlocked: null, +})) + +// Import mocked modules +import { useIsWalletUnlocked } from 'src/app/hooks/useIsWalletUnlocked' +import { useIsChromeWindowFocused } from 'uniswap/src/extension/useIsChromeWindowFocused' + +const mockUseIsChromeWindowFocused = jest.mocked(useIsChromeWindowFocused) +const mockUseIsWalletUnlocked = jest.mocked(useIsWalletUnlocked) +const mockLogger = jest.mocked(logger) + +// Mock chrome.alarms API +const mockChromeAlarms = { + create: jest.fn(), + clear: jest.fn(), } -const simulateFocusChange = (component: ReturnType) => (fromFocused: boolean, toFocused: boolean) => { - mockUseIsChromeWindowFocusedWithTimeout.mockReturnValue(fromFocused) - const { rerender } = component +global.chrome = { + ...global.chrome, + alarms: mockChromeAlarms as unknown as typeof chrome.alarms, +} - mockUseIsChromeWindowFocusedWithTimeout.mockReturnValue(toFocused) - rerender( +// Helper function +const renderAutoLockProvider = (deviceAccessTimeout: DeviceAccessTimeout) => { + return render(
Test
, + { + preloadedState: { + userSettings: { + currentLanguage: Language.English, + currentCurrency: FiatCurrency.UnitedStatesDollar, + hideSmallBalances: true, + hideSpamTokens: true, + hapticsEnabled: true, + deviceAccessTimeout, + }, + }, + }, ) } -const expectWalletLockCalled = async (times: number = 1) => { - await waitFor(() => { - expect(mockKeyring.lock).toHaveBeenCalledTimes(times) - }) -} +const simulateFocusChange = (component: ReturnType) => (fromFocused: boolean, toFocused: boolean) => { + mockUseIsChromeWindowFocused.mockReturnValue(fromFocused) + const { rerender } = component -const expectAnalyticsEventCalled = async () => { - await waitFor(() => { - expect(mockSendAnalyticsEvent).toHaveBeenCalledWith(ExtensionEventName.ChangeLockedState, { - locked: true, - location: 'background', - }) - }) + mockUseIsChromeWindowFocused.mockReturnValue(toFocused) + rerender() } describe('AutoLockProvider', () => { beforeEach(() => { jest.clearAllMocks() - mockUseIsChromeWindowFocusedWithTimeout.mockReturnValue(true) - mockKeyring.lock.mockResolvedValue(true) - mockSendAnalyticsEvent.mockImplementation(() => {}) + mockUseIsChromeWindowFocused.mockReturnValue(true) + mockUseIsWalletUnlocked.mockReturnValue(true) mockLogger.debug.mockImplementation(() => {}) mockLogger.error.mockImplementation(() => {}) + mockChromeAlarms.create.mockImplementation(() => {}) + mockChromeAlarms.clear.mockImplementation(() => {}) }) - describe('window focus monitoring', () => { - const testTimeoutValues = [ - { timeout: DeviceAccessTimeout.FiveMinutes, expectedMs: 5 * 60 * 1000, description: '5 minutes' }, - { timeout: DeviceAccessTimeout.ThirtyMinutes, expectedMs: 30 * 60 * 1000, description: '30 minutes' }, - { timeout: DeviceAccessTimeout.OneHour, expectedMs: 60 * 60 * 1000, description: '1 hour' }, - { timeout: DeviceAccessTimeout.TwentyFourHours, expectedMs: 24 * 60 * 60 * 1000, description: '24 hours' }, - { - timeout: DeviceAccessTimeout.Never, - expectedMs: Number.MAX_SAFE_INTEGER, - description: 'Never (MAX_SAFE_INTEGER)', - }, - ] + describe('mount behavior', () => { + it('should clear alarm on mount', () => { + renderAutoLockProvider(DeviceAccessTimeout.FiveMinutes) - testTimeoutValues.forEach(({ timeout, expectedMs, description }) => { - it(`should call useIsChromeWindowFocusedWithTimeout with correct timeout for ${description}`, () => { - renderAutoLockProvider(timeout) - expect(mockUseIsChromeWindowFocusedWithTimeout).toHaveBeenCalledWith(expectedMs) - }) + expect(mockChromeAlarms.clear).toHaveBeenCalledWith('AutoLockAlarm') + }) + + it('should always render children', () => { + const { container } = renderAutoLockProvider(DeviceAccessTimeout.FiveMinutes) + expect(container.textContent).toBe('Test') }) }) - describe('wallet locking behavior', () => { - it('should lock wallet when window loses focus and timeout is configured', async () => { + describe('unmount behavior', () => { + it('should not schedule alarm on unmount (handled by background port disconnect)', () => { + const { unmount } = renderAutoLockProvider(DeviceAccessTimeout.FiveMinutes) + + unmount() + + // Unmount no longer schedules alarm - this is handled by background script + expect(mockChromeAlarms.create).not.toHaveBeenCalled() + }) + }) + + describe('focus change behavior (while sidebar is open)', () => { + it('should schedule alarm when window loses focus and wallet is unlocked', () => { const component = renderAutoLockProvider(DeviceAccessTimeout.FiveMinutes) - expect(mockKeyring.lock).not.toHaveBeenCalled() + mockChromeAlarms.create.mockClear() // Clear the mount call simulateFocusChange(component)(true, false) - await expectWalletLockCalled() + + expect(mockChromeAlarms.create).toHaveBeenCalledWith('AutoLockAlarm', { + delayInMinutes: 5, + }) }) - it('should not lock wallet when window is focused', () => { - mockUseIsChromeWindowFocusedWithTimeout.mockReturnValue(true) - renderAutoLockProvider(DeviceAccessTimeout.FiveMinutes) - expect(mockKeyring.lock).not.toHaveBeenCalled() + it('should clear alarm when window regains focus', () => { + const component = renderAutoLockProvider(DeviceAccessTimeout.FiveMinutes) + mockChromeAlarms.clear.mockClear() // Clear the mount call + + // First lose focus (creates alarm) + simulateFocusChange(component)(true, false) + expect(mockChromeAlarms.create).toHaveBeenCalled() + + // Then regain focus + simulateFocusChange(component)(false, true) + + expect(mockChromeAlarms.clear).toHaveBeenCalledWith('AutoLockAlarm') }) - it('should not lock wallet when timeout is set to Never', () => { - mockUseIsChromeWindowFocusedWithTimeout.mockReturnValue(false) - renderAutoLockProvider(DeviceAccessTimeout.Never) - expect(mockKeyring.lock).not.toHaveBeenCalled() + it('should not schedule alarm when window loses focus and wallet is locked', () => { + mockUseIsWalletUnlocked.mockReturnValue(false) + const component = renderAutoLockProvider(DeviceAccessTimeout.FiveMinutes) + mockChromeAlarms.create.mockClear() // Clear the mount call + + simulateFocusChange(component)(true, false) + + expect(mockChromeAlarms.create).not.toHaveBeenCalled() }) - it('should send analytics event when locking wallet', async () => { - const component = renderAutoLockProvider(DeviceAccessTimeout.ThirtyMinutes) + it('should not schedule alarm when window loses focus and timeout is Never', () => { + const component = renderAutoLockProvider(DeviceAccessTimeout.Never) + mockChromeAlarms.create.mockClear() // Clear the mount call + simulateFocusChange(component)(true, false) - await expectAnalyticsEventCalled() - expect(mockSendAnalyticsEvent).toHaveBeenCalledTimes(1) + + expect(mockChromeAlarms.create).not.toHaveBeenCalled() }) }) - describe('error handling', () => { - it('should handle Keyring.lock() errors gracefully', async () => { - const lockError = new Error('Failed to lock keyring') - mockKeyring.lock.mockRejectedValue(lockError) + describe('wallet state changes', () => { + it('should clear alarm when wallet becomes locked', () => { + mockUseIsWalletUnlocked.mockReturnValue(true) + const { rerender } = renderAutoLockProvider(DeviceAccessTimeout.FiveMinutes) - const component = renderAutoLockProvider(DeviceAccessTimeout.FiveMinutes) - simulateFocusChange(component)(true, false) + // Clear the initial mount call + mockChromeAlarms.clear.mockClear() - await waitFor(() => { - expect(mockLogger.error).toHaveBeenCalledWith(lockError, { - tags: { - file: 'AutoLockProvider.tsx', - function: 'lockWallet', - }, - }) - }) + // Wallet becomes locked + mockUseIsWalletUnlocked.mockReturnValue(false) + rerender() + + expect(mockChromeAlarms.clear).toHaveBeenCalledWith('AutoLockAlarm') }) - it('should not send analytics event when lock fails', async () => { - const lockError = new Error('Failed to lock keyring') - mockKeyring.lock.mockRejectedValue(lockError) + it('should clear alarm when wallet becomes unlocked', () => { + mockUseIsWalletUnlocked.mockReturnValue(false) + const { rerender } = renderAutoLockProvider(DeviceAccessTimeout.FiveMinutes) - const component = renderAutoLockProvider(DeviceAccessTimeout.FiveMinutes) - simulateFocusChange(component)(true, false) + // Clear the initial mount call + mockChromeAlarms.clear.mockClear() - await waitFor(() => { - expect(mockLogger.error).toHaveBeenCalledWith(lockError, { - tags: { - file: 'AutoLockProvider.tsx', - function: 'lockWallet', - }, - }) - }) + // Wallet becomes unlocked + mockUseIsWalletUnlocked.mockReturnValue(true) + rerender() - expect(mockSendAnalyticsEvent).not.toHaveBeenCalled() - }) - - it('should handle undefined deviceAccessTimeout gracefully', () => { - render( - -
Test
-
, - { - preloadedState: { - userSettings: { - currentLanguage: Language.English, - currentCurrency: FiatCurrency.UnitedStatesDollar, - hideSmallBalances: true, - hideSpamTokens: true, - hapticsEnabled: true, - deviceAccessTimeout: undefined as any, - }, - }, - }, - ) + expect(mockChromeAlarms.clear).toHaveBeenCalledWith('AutoLockAlarm') + }) + + it('should not clear alarm on initial render (only mount clear)', () => { + mockUseIsWalletUnlocked.mockReturnValue(true) + renderAutoLockProvider(DeviceAccessTimeout.FiveMinutes) + + // Only the mount clear should have been called + expect(mockChromeAlarms.clear).toHaveBeenCalledTimes(1) + }) + }) + + describe('combined scenarios', () => { + it('should handle mount -> unmount -> remount correctly', () => { + // First mount + const { unmount } = renderAutoLockProvider(DeviceAccessTimeout.FiveMinutes) + expect(mockChromeAlarms.clear).toHaveBeenCalledTimes(1) + + // Unmount (alarm scheduling now handled in background) + unmount() + + // Second mount (clears alarm) + renderAutoLockProvider(DeviceAccessTimeout.FiveMinutes) + expect(mockChromeAlarms.clear).toHaveBeenCalledTimes(2) + }) + + it('should handle wallet unlock during mounted state', () => { + mockUseIsWalletUnlocked.mockReturnValue(false) + const { rerender } = renderAutoLockProvider(DeviceAccessTimeout.FiveMinutes) + + mockChromeAlarms.clear.mockClear() + + // Wallet unlocks while mounted + mockUseIsWalletUnlocked.mockReturnValue(true) + rerender() + + // Should clear alarm due to wallet state change + expect(mockChromeAlarms.clear).toHaveBeenCalled() + }) - expect(mockUseIsChromeWindowFocusedWithTimeout).toHaveBeenCalledWith(30 * 60 * 1000) + it('should handle wallet lock during mounted state', () => { + mockUseIsWalletUnlocked.mockReturnValue(true) + const { rerender } = renderAutoLockProvider(DeviceAccessTimeout.FiveMinutes) + + mockChromeAlarms.clear.mockClear() + + // Wallet locks while mounted + mockUseIsWalletUnlocked.mockReturnValue(false) + rerender() + + // Should clear alarm due to wallet state change + expect(mockChromeAlarms.clear).toHaveBeenCalled() }) }) - describe('focus state changes', () => { - it('should react to focus state changes from focused to unfocused', async () => { + describe('error handling', () => { + it('should handle chrome.alarms.create errors gracefully', () => { + const error = new Error('Permission denied') + mockChromeAlarms.create.mockImplementationOnce(() => { + throw error + }) + const component = renderAutoLockProvider(DeviceAccessTimeout.FiveMinutes) - expect(mockKeyring.lock).not.toHaveBeenCalled() + mockChromeAlarms.create.mockClear() - simulateFocusChange(component)(true, false) - await expectWalletLockCalled() + // This should not throw, error should be logged + expect(() => { + simulateFocusChange(component)(true, false) + }).not.toThrow() + + expect(mockLogger.error).toHaveBeenCalledWith(error, { + tags: { file: 'AutoLockProvider', function: 'createAutoLockAlarm' }, + extra: { delayInMinutes: 5 }, + }) + }) + + it('should handle chrome.alarms.clear errors gracefully', () => { + const error = new Error('Permission denied') + mockChromeAlarms.clear.mockImplementationOnce(() => { + throw error + }) + + // This should not throw, error should be logged + expect(() => { + renderAutoLockProvider(DeviceAccessTimeout.FiveMinutes) + }).not.toThrow() + + expect(mockLogger.error).toHaveBeenCalledWith(error, { + tags: { file: 'AutoLockProvider', function: 'clearAutoLockAlarm' }, + extra: { reason: 'Cleared auto-lock alarm (sidebar opened)' }, + }) }) - it('should not trigger multiple locks when already unfocused', async () => { + it('should continue to function after chrome.alarms errors', () => { + // First call fails + mockChromeAlarms.clear.mockImplementationOnce(() => { + throw new Error('Permission denied') + }) + const component = renderAutoLockProvider(DeviceAccessTimeout.FiveMinutes) + // Clear the error and mock calls + mockLogger.error.mockClear() + mockChromeAlarms.clear.mockClear() + mockChromeAlarms.create.mockClear() + + // Subsequent calls should still work simulateFocusChange(component)(true, false) - await expectWalletLockCalled() + expect(mockChromeAlarms.create).toHaveBeenCalledWith('AutoLockAlarm', { + delayInMinutes: 5, + }) + expect(mockLogger.error).not.toHaveBeenCalled() + }) + }) - // Rerender with same unfocused state - should not trigger additional calls - const { rerender } = component - rerender( - -
Test
-
, - ) + describe('edge cases', () => { + it('should handle rapid mount/unmount cycles', () => { + const { unmount: unmount1 } = renderAutoLockProvider(DeviceAccessTimeout.FiveMinutes) + unmount1() - expect(mockKeyring.lock).toHaveBeenCalledTimes(1) + const { unmount: unmount2 } = renderAutoLockProvider(DeviceAccessTimeout.FiveMinutes) + unmount2() + + // Should have cleared alarm twice (once per mount) + expect(mockChromeAlarms.clear).toHaveBeenCalledTimes(2) + // No create calls because unmount scheduling is handled in background + expect(mockChromeAlarms.create).not.toHaveBeenCalled() }) - it('should not lock again when returning to focused state', async () => { + it('should handle rapid focus changes', () => { const component = renderAutoLockProvider(DeviceAccessTimeout.FiveMinutes) + mockChromeAlarms.create.mockClear() + mockChromeAlarms.clear.mockClear() + // Lose focus simulateFocusChange(component)(true, false) - await expectWalletLockCalled() + expect(mockChromeAlarms.create).toHaveBeenCalledTimes(1) - // Change back to focused + // Regain focus simulateFocusChange(component)(false, true) - expect(mockKeyring.lock).toHaveBeenCalledTimes(1) - }) - }) + expect(mockChromeAlarms.clear).toHaveBeenCalledTimes(1) - describe('timeout setting changes', () => { - it('should respond to timeout setting changes', () => { - renderAutoLockProvider(DeviceAccessTimeout.FiveMinutes) - expect(mockUseIsChromeWindowFocusedWithTimeout).toHaveBeenLastCalledWith(5 * 60 * 1000) - - renderAutoLockProvider(DeviceAccessTimeout.OneHour) - expect(mockUseIsChromeWindowFocusedWithTimeout).toHaveBeenLastCalledWith(60 * 60 * 1000) + // Lose focus again + simulateFocusChange(component)(true, false) + expect(mockChromeAlarms.create).toHaveBeenCalledTimes(2) }) - it('should handle timeout change from configured to Never', () => { - mockUseIsChromeWindowFocusedWithTimeout.mockReturnValue(false) + it('should prioritize wallet state changes over focus changes (race condition)', () => { + mockUseIsWalletUnlocked.mockReturnValue(true) + mockUseIsChromeWindowFocused.mockReturnValue(true) + const { rerender } = renderAutoLockProvider(DeviceAccessTimeout.FiveMinutes) - renderAutoLockProvider(DeviceAccessTimeout.FiveMinutes) - expect(mockUseIsChromeWindowFocusedWithTimeout).toHaveBeenCalledWith(5 * 60 * 1000) + mockChromeAlarms.clear.mockClear() + mockChromeAlarms.create.mockClear() + + // Simulate simultaneous wallet lock + focus loss + mockUseIsWalletUnlocked.mockReturnValue(false) + mockUseIsChromeWindowFocused.mockReturnValue(false) + rerender() - renderAutoLockProvider(DeviceAccessTimeout.Never) - expect(mockUseIsChromeWindowFocusedWithTimeout).toHaveBeenLastCalledWith(Number.MAX_SAFE_INTEGER) + // Should only clear alarm (wallet state change priority), not schedule + expect(mockChromeAlarms.clear).toHaveBeenCalledTimes(1) + expect(mockChromeAlarms.create).not.toHaveBeenCalled() }) }) }) diff --git a/apps/extension/src/app/components/tabs/ActivityTab.tsx b/apps/extension/src/app/components/tabs/ActivityTab.tsx index a0a8074467f..95ca43b900f 100644 --- a/apps/extension/src/app/components/tabs/ActivityTab.tsx +++ b/apps/extension/src/app/components/tabs/ActivityTab.tsx @@ -1,5 +1,6 @@ import { memo } from 'react' -import { ScrollView } from 'ui/src' +import { Flex, Loader, ScrollView } from 'ui/src' +import { useInfiniteScroll } from 'utilities/src/react/useInfiniteScroll' import { useActivityDataWallet } from 'wallet/src/features/activity/useActivityDataWallet' export const ActivityTab = memo(function _ActivityTab({ @@ -9,9 +10,16 @@ export const ActivityTab = memo(function _ActivityTab({ address: Address skip?: boolean }): JSX.Element { - const { maybeEmptyComponent, renderActivityItem, sectionData } = useActivityDataWallet({ - evmOwner: address, - skip, + const { maybeEmptyComponent, renderActivityItem, sectionData, fetchNextPage, hasNextPage, isFetchingNextPage } = + useActivityDataWallet({ + evmOwner: address, + skip, + }) + + const { sentinelRef } = useInfiniteScroll({ + onLoadMore: fetchNextPage, + hasNextPage, + isFetching: isFetchingNextPage, }) if (maybeEmptyComponent) { @@ -22,6 +30,14 @@ export const ActivityTab = memo(function _ActivityTab({ {/* `sectionData` will be either an array of transactions or an array of loading skeletons */} {sectionData.map((item, index) => renderActivityItem({ item, index }))} + {/* Show skeleton loading indicator while fetching next page */} + {isFetchingNextPage && ( + + + + )} + {/* Intersection observer sentinel for infinite scroll */} + ) }) diff --git a/apps/extension/src/app/core/BaseAppContainer.tsx b/apps/extension/src/app/core/BaseAppContainer.tsx index 09f9327325c..c29cba1056e 100644 --- a/apps/extension/src/app/core/BaseAppContainer.tsx +++ b/apps/extension/src/app/core/BaseAppContainer.tsx @@ -1,3 +1,14 @@ +import { ApiInit, getEntryGatewayUrl, provideSessionService } from '@universe/api' +import { + getIsSessionServiceEnabled, + getIsSessionUpgradeAutoEnabled, + useIsSessionServiceEnabled, +} from '@universe/gating' +import { + createChallengeSolverService, + createSessionInitializationService, + SessionInitializationService, +} from '@universe/sessions' import { PropsWithChildren } from 'react' import { I18nextProvider } from 'react-i18next' import { GraphqlProvider } from 'src/app/apollo' @@ -14,6 +25,46 @@ import { ErrorBoundary } from 'wallet/src/components/ErrorBoundary/ErrorBoundary import { AccountsStoreContextProvider } from 'wallet/src/features/accounts/store/provider' import { SharedWalletProvider } from 'wallet/src/providers/SharedWalletProvider' +const provideSessionInitializationService = (): SessionInitializationService => + createSessionInitializationService({ + getSessionService: () => + provideSessionService({ + getBaseUrl: getEntryGatewayUrl, + getIsSessionServiceEnabled, + }), + challengeSolverService: createChallengeSolverService(), + getIsSessionUpgradeAutoEnabled, + }) + +function BaseAppContainerInner({ children }: PropsWithChildren): JSX.Element { + const isSessionServiceEnabled = useIsSessionServiceEnabled() + + return ( + + + + + + + + + + + {children} + + + + + + + + + ) +} + export function BaseAppContainer({ children, appName, @@ -21,24 +72,7 @@ export function BaseAppContainer({ return ( - - - - - - - - - - {children} - - - - - - - - + {children} ) diff --git a/apps/extension/src/app/core/SidebarApp.tsx b/apps/extension/src/app/core/SidebarApp.tsx index db280c45dfb..eeadc95bfa9 100644 --- a/apps/extension/src/app/core/SidebarApp.tsx +++ b/apps/extension/src/app/core/SidebarApp.tsx @@ -6,6 +6,7 @@ import { useEffect, useRef, useState } from 'react' import { useDispatch } from 'react-redux' import { createHashRouter, RouterProvider } from 'react-router' import { PersistGate } from 'redux-persist/integration/react' +import { AUTO_LOCK_ALARM_NAME } from 'src/app/components/AutoLockProvider' import { ErrorElement } from 'src/app/components/ErrorElement' import { useTraceSidebarDappUrl } from 'src/app/components/Trace/useTraceSidebarDappUrl' import { BaseAppContainer } from 'src/app/core/BaseAppContainer' @@ -188,8 +189,21 @@ function useDappRequestPortListener(): void { }, PORT_PING_INTERVAL) } +/** + * Creates a connection so that the background script can detect when the sidebar is closed and schedule an auto-lock alarm. + */ +function useAutoLockAlarmConnection(): void { + useEffect(() => { + const port = chrome.runtime.connect({ name: AUTO_LOCK_ALARM_NAME }) + return () => { + port.disconnect() + } + }, []) +} + function SidebarWrapper(): JSX.Element { useDappRequestPortListener() + useAutoLockAlarmConnection() useTestnetModeForLoggingAndAnalytics() const resetUnitagsQueries = useResetUnitagsQueries() diff --git a/apps/extension/src/app/features/accounts/AccountItem.tsx b/apps/extension/src/app/features/accounts/AccountItem.tsx index dc2db86aa01..99eb7319601 100644 --- a/apps/extension/src/app/features/accounts/AccountItem.tsx +++ b/apps/extension/src/app/features/accounts/AccountItem.tsx @@ -4,8 +4,8 @@ import { useTranslation } from 'react-i18next' import { useDispatch } from 'react-redux' import { EditLabelModal } from 'src/app/features/accounts/EditLabelModal' import { removeAllDappConnectionsForAccount } from 'src/app/features/dapp/actions' -import { AppRoutes, SettingsRoutes } from 'src/app/navigation/constants' -import { useExtensionNavigation } from 'src/app/navigation/utils' +import { AppRoutes, SettingsRoutes, UnitagClaimRoutes } from 'src/app/navigation/constants' +import { focusOrCreateUnitagTab, useExtensionNavigation } from 'src/app/navigation/utils' import { Flex, Text, TouchableArea } from 'ui/src' import { CopySheets, Edit, Ellipsis, Globe, TrashFilled } from 'ui/src/components/icons' import { iconSizes } from 'ui/src/theme' @@ -97,13 +97,17 @@ export function AccountItem({ address, onAccountSelect, balanceUSD }: AccountIte label: !accountHasUnitag ? t('account.wallet.menu.edit.title') : t('settings.setting.wallet.action.editProfile'), - onPress: (e: BaseSyntheticEvent): void => { + onPress: async (e: BaseSyntheticEvent): Promise => { // We have to manually prevent click-through because the way the context menu is inside of a TouchableArea in this component it // means that without it the TouchableArea handler will get called e.preventDefault() e.stopPropagation() - setShowEditLabelModal(true) + if (accountHasUnitag) { + await focusOrCreateUnitagTab(address, UnitagClaimRoutes.EditProfile) + } else { + setShowEditLabelModal(true) + } }, Icon: Edit, }, @@ -134,7 +138,7 @@ export function AccountItem({ address, onAccountSelect, balanceUSD }: AccountIte iconProps: { color: '$statusCritical' }, }, ] - }, [accountHasUnitag, onPressCopyAddress, navigateTo, t]) + }, [accountHasUnitag, onPressCopyAddress, navigateTo, t, address]) return ( <> diff --git a/apps/extension/src/app/features/accounts/AccountSwitcherScreen.tsx b/apps/extension/src/app/features/accounts/AccountSwitcherScreen.tsx index f73975084a1..41a94dd16f6 100644 --- a/apps/extension/src/app/features/accounts/AccountSwitcherScreen.tsx +++ b/apps/extension/src/app/features/accounts/AccountSwitcherScreen.tsx @@ -1,3 +1,4 @@ +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { BaseSyntheticEvent, useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' @@ -18,9 +19,13 @@ import { Button, Flex, Popover, ScrollView, Text, TouchableArea, useSporeColors import { Ellipsis, Globe, Person, TrashFilled, WalletFilled, X } from 'ui/src/components/icons' import { spacing } from 'ui/src/theme' import { AddressDisplay } from 'uniswap/src/components/accounts/AddressDisplay' +import { buildWrappedUrl } from 'uniswap/src/components/banners/shared/utils' +import { UniswapWrapped2025Card } from 'uniswap/src/components/banners/UniswapWrapped2025Card/UniswapWrapped2025Card' import { WarningSeverity } from 'uniswap/src/components/modals/WarningModal/types' import { WarningModal } from 'uniswap/src/components/modals/WarningModal/WarningModal' +import { UNISWAP_WEB_URL } from 'uniswap/src/constants/urls' import { AccountType, DisplayNameType } from 'uniswap/src/features/accounts/types' +import { setHasDismissedUniswapWrapped2025Banner } from 'uniswap/src/features/behaviorHistory/slice' import { Platform } from 'uniswap/src/features/platforms/types/Platform' import { ModalName, WalletEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' @@ -60,6 +65,8 @@ export function AccountSwitcherScreen(): JSX.Element { const activeAddress = activeAccount.address const isViewOnly = activeAccount.type === AccountType.Readonly + const isWrappedBannerEnabled = useFeatureFlag(FeatureFlags.UniswapWrapped2025) + const accounts = useSignerAccounts() const accountAddresses = useMemo( () => @@ -154,6 +161,17 @@ export function AccountSwitcherScreen(): JSX.Element { [connectedAccounts.length, dispatch, pendingWallet], ) + const onPressWrappedCard = useCallback(() => { + try { + const url = buildWrappedUrl(UNISWAP_WEB_URL, activeAddress) + window.open(url, '_blank') + dispatch(setHasDismissedUniswapWrapped2025Banner(true)) + navigate(-1) + } catch (error) { + logger.error(error, { tags: { file: 'AccountSwitcherScreen', function: 'onPressWrappedCard' } }) + } + }, [activeAddress, dispatch]) + const addWalletMenuOptions: MenuContentItem[] = [ { label: t('account.wallet.button.create'), @@ -287,6 +305,12 @@ export function AccountSwitcherScreen(): JSX.Element {
+ {isWrappedBannerEnabled && ( + + + + )} + {activeAccountHasUnitag ? ( @@ -355,7 +379,7 @@ export function AccountSwitcherScreen(): JSX.Element { borderColor="$surface3" borderRadius="$rounded16" borderWidth="$spacing1" - disableRemoveScroll={false} + enableRemoveScroll={true} enterStyle={{ y: -10, opacity: 0 }} exitStyle={{ y: -10, opacity: 0 }} p="$none" diff --git a/apps/extension/src/app/features/accounts/__snapshots__/AccountSwitcherScreen.test.tsx.snap b/apps/extension/src/app/features/accounts/__snapshots__/AccountSwitcherScreen.test.tsx.snap index 4541332866b..eed50298533 100644 --- a/apps/extension/src/app/features/accounts/__snapshots__/AccountSwitcherScreen.test.tsx.snap +++ b/apps/extension/src/app/features/accounts/__snapshots__/AccountSwitcherScreen.test.tsx.snap @@ -15,7 +15,7 @@ exports[`AccountSwitcherScreen renders correctly 1`] = `

@@ -25,7 +25,7 @@ exports[`AccountSwitcherScreen renders correctly 1`] = `

@@ -35,7 +35,7 @@ exports[`AccountSwitcherScreen renders correctly 1`] = `

@@ -47,8 +47,10 @@ exports[`AccountSwitcherScreen renders correctly 1`] = ` >
-
-
+
- - - - + + + + + +
@@ -139,6 +148,8 @@ exports[`AccountSwitcherScreen renders correctly 1`] = ` data-testid="account-icon" >
@@ -312,6 +334,45 @@ exports[`AccountSwitcherScreen renders correctly 1`] = ` />
+ + + + + + + + + + + + + + + , "container":
@@ -334,7 +395,7 @@ exports[`AccountSwitcherScreen renders correctly 1`] = `

@@ -344,7 +405,7 @@ exports[`AccountSwitcherScreen renders correctly 1`] = `

@@ -356,8 +417,10 @@ exports[`AccountSwitcherScreen renders correctly 1`] = ` >
-
-
+
- - - - + + + + + +
@@ -448,6 +518,8 @@ exports[`AccountSwitcherScreen renders correctly 1`] = ` data-testid="account-icon" >
diff --git a/apps/extension/src/app/features/biometricUnlock/BiometricUnlockStorage.ts b/apps/extension/src/app/features/biometricUnlock/BiometricUnlockStorage.ts index bd06afb9236..599a36b1c07 100644 --- a/apps/extension/src/app/features/biometricUnlock/BiometricUnlockStorage.ts +++ b/apps/extension/src/app/features/biometricUnlock/BiometricUnlockStorage.ts @@ -4,6 +4,7 @@ import { PersistedStorage } from 'wallet/src/utils/persistedStorage' export type BiometricUnlockStorageData = { credentialId: string + transports: AuthenticatorTransport[] secretPayload: Omit & { ciphertext: string } } diff --git a/apps/extension/src/app/features/biometricUnlock/biometricAuthUtils.ts b/apps/extension/src/app/features/biometricUnlock/biometricAuthUtils.ts index d82aeb49f58..21aa3d19515 100644 --- a/apps/extension/src/app/features/biometricUnlock/biometricAuthUtils.ts +++ b/apps/extension/src/app/features/biometricUnlock/biometricAuthUtils.ts @@ -16,9 +16,11 @@ import { */ export async function authenticateWithBiometricCredential({ credentialId, + transports, abortSignal, }: { credentialId: string + transports: AuthenticatorTransport[] abortSignal: AbortSignal }): Promise<{ publicKeyCredential: PublicKeyCredential; encryptionKey: CryptoKey }> { // Convert stored credential ID back to binary format @@ -31,6 +33,7 @@ export async function authenticateWithBiometricCredential({ { type: 'public-key', id: credentialIdBuffer, + transports, }, ], userVerification: 'required', @@ -70,10 +73,12 @@ export async function encryptPasswordWithBiometricData({ password, encryptionKey, credentialId, + transports, }: { password: string encryptionKey: CryptoKey credentialId: string + transports: AuthenticatorTransport[] }): Promise { // Create a new secret payload for the password const secretPayload = await createEmptySecretPayload() @@ -86,7 +91,7 @@ export async function encryptPasswordWithBiometricData({ additionalData: credentialId, // Use credential ID as additional authenticated data }) - return { credentialId, secretPayload: secretPayloadWithCiphertext } + return { credentialId, transports, secretPayload: secretPayloadWithCiphertext } } /** diff --git a/apps/extension/src/app/features/biometricUnlock/useBiometricUnlockSetupMutation.test.ts b/apps/extension/src/app/features/biometricUnlock/useBiometricUnlockSetupMutation.test.ts index 1b15deca6b5..1eb0c37cb0b 100644 --- a/apps/extension/src/app/features/biometricUnlock/useBiometricUnlockSetupMutation.test.ts +++ b/apps/extension/src/app/features/biometricUnlock/useBiometricUnlockSetupMutation.test.ts @@ -42,7 +42,12 @@ const mockGetEncryptionKeyFromBuffer = jest.requireMock( // Mock PublicKeyCredential (doesn't exist in Jest environment) class MockPublicKeyCredential { - constructor(public rawId: ArrayBuffer) {} + constructor( + public rawId: ArrayBuffer, + public response = { + getTransports: () => ['internal' as AuthenticatorTransport], + }, + ) {} } Object.defineProperty(global, 'PublicKeyCredential', { writable: true, @@ -124,10 +129,12 @@ describe('useBiometricUnlockSetupMutation', () => { // Verify the stored secret payload has all required properties const storedData = mockBiometricUnlockStorage.set.mock.calls[0]![0] as { credentialId: string + transports: AuthenticatorTransport[] secretPayload: typeof mockSecretPayload } expect(storedData.credentialId).toBe(expectedCredentialId) + expect(storedData.transports).toEqual(['internal']) expect(storedData.secretPayload).toEqual( expect.objectContaining({ diff --git a/apps/extension/src/app/features/biometricUnlock/useBiometricUnlockSetupMutation.ts b/apps/extension/src/app/features/biometricUnlock/useBiometricUnlockSetupMutation.ts index 345545a74c8..bd0e4616f51 100644 --- a/apps/extension/src/app/features/biometricUnlock/useBiometricUnlockSetupMutation.ts +++ b/apps/extension/src/app/features/biometricUnlock/useBiometricUnlockSetupMutation.ts @@ -69,7 +69,7 @@ async function createCredentialAndEncryptPassword({ const rawKey = await window.crypto.subtle.exportKey('raw', encryptionKey) - const { credentialId } = await createCredential({ + const { credentialId, transports } = await createCredential({ encryptionKey: rawKey, abortSignal, }) @@ -78,6 +78,7 @@ async function createCredentialAndEncryptPassword({ password, encryptionKey, credentialId, + transports, }) } @@ -116,7 +117,7 @@ async function createCredential({ }: { encryptionKey: ArrayBuffer abortSignal: AbortSignal -}): Promise<{ credentialId: string }> { +}): Promise<{ credentialId: string; transports: AuthenticatorTransport[] }> { // Create WebAuthn credential with platform authenticator (Touch ID, Windows Hello, etc.) forced const credential = await navigator.credentials.create({ publicKey: { @@ -149,5 +150,8 @@ async function createCredential({ // Convert raw ID to a storable string format const credentialId = btoa(String.fromCharCode(...new Uint8Array(publicKeyCredential.rawId))) - return { credentialId } + const response = publicKeyCredential.response as AuthenticatorAttestationResponse + const transports = response.getTransports() as AuthenticatorTransport[] + + return { credentialId, transports } } diff --git a/apps/extension/src/app/features/biometricUnlock/useChangePasswordWithBiometricMutation.test.ts b/apps/extension/src/app/features/biometricUnlock/useChangePasswordWithBiometricMutation.test.ts index fd312c48e6a..c8f23d90136 100644 --- a/apps/extension/src/app/features/biometricUnlock/useChangePasswordWithBiometricMutation.test.ts +++ b/apps/extension/src/app/features/biometricUnlock/useChangePasswordWithBiometricMutation.test.ts @@ -108,6 +108,7 @@ describe('useChangePasswordWithBiometricMutation', () => { // Setup default mocks mockBiometricUnlockStorage.get.mockResolvedValue({ credentialId: mockCredentialId, + transports: ['internal'], secretPayload: mockOldEncryptedPayload, }) @@ -146,6 +147,7 @@ describe('useChangePasswordWithBiometricMutation', () => { { type: 'public-key', id: credentialIdBuffer, + transports: ['internal'], }, ], userVerification: 'required', @@ -160,6 +162,7 @@ describe('useChangePasswordWithBiometricMutation', () => { // 4. Should update the stored biometric data with re-encrypted password expect(mockBiometricUnlockStorage.set).toHaveBeenCalledWith({ credentialId: mockCredentialId, + transports: ['internal'], secretPayload: expect.objectContaining({ ciphertext: expect.any(String), iv: expect.any(String), @@ -189,6 +192,7 @@ describe('useChangePasswordWithBiometricMutation', () => { const newBiometricData = setCall?.[0] expect(newBiometricData?.credentialId).toBe(mockCredentialId) + expect(newBiometricData?.transports).toEqual(['internal']) expect(newBiometricData?.secretPayload).toMatchObject({ ciphertext: expect.any(String), iv: expect.any(String), diff --git a/apps/extension/src/app/features/biometricUnlock/useChangePasswordWithBiometricMutation.ts b/apps/extension/src/app/features/biometricUnlock/useChangePasswordWithBiometricMutation.ts index 8da478fe45c..11361bdf03f 100644 --- a/apps/extension/src/app/features/biometricUnlock/useChangePasswordWithBiometricMutation.ts +++ b/apps/extension/src/app/features/biometricUnlock/useChangePasswordWithBiometricMutation.ts @@ -25,6 +25,7 @@ export function useChangePasswordWithBiometricMutation(options?: { // Authenticate with WebAuthn to get the encryption key const { encryptionKey } = await authenticateWithBiometricCredential({ credentialId: biometricUnlockCredential.credentialId, + transports: biometricUnlockCredential.transports, abortSignal, }) @@ -36,6 +37,7 @@ export function useChangePasswordWithBiometricMutation(options?: { password: newPassword, encryptionKey, credentialId: biometricUnlockCredential.credentialId, + transports: biometricUnlockCredential.transports, }) // Update the stored biometric data diff --git a/apps/extension/src/app/features/biometricUnlock/useUnlockWithBiometricCredentialMutation.test.ts b/apps/extension/src/app/features/biometricUnlock/useUnlockWithBiometricCredentialMutation.test.ts index 832e727c382..5685ba37f1a 100644 --- a/apps/extension/src/app/features/biometricUnlock/useUnlockWithBiometricCredentialMutation.test.ts +++ b/apps/extension/src/app/features/biometricUnlock/useUnlockWithBiometricCredentialMutation.test.ts @@ -3,15 +3,13 @@ import { waitFor } from '@testing-library/react' import { BiometricUnlockStorage } from 'src/app/features/biometricUnlock/BiometricUnlockStorage' import { useUnlockWithBiometricCredentialMutation } from 'src/app/features/biometricUnlock/useUnlockWithBiometricCredentialMutation' import { renderHookWithProviders } from 'src/test/render' -import { authActions } from 'wallet/src/features/auth/saga' -import { AuthActionType } from 'wallet/src/features/auth/types' import { encodeForStorage, encrypt, generateNew256BitRandomBuffer } from 'wallet/src/features/wallet/Keyring/crypto' jest.mock('src/app/features/biometricUnlock/BiometricUnlockStorage') -jest.mock('wallet/src/features/auth/saga', () => ({ - authActions: { - trigger: jest.fn(), - }, + +const mockUnlockWithPassword = jest.fn() +jest.mock('src/app/features/lockScreen/useUnlockWithPassword', () => ({ + useUnlockWithPassword: jest.fn(() => mockUnlockWithPassword), })) // Mock the Web Crypto API with Node.js built-in @@ -27,7 +25,6 @@ Object.defineProperty(navigator, 'credentials', { }) const mockBiometricUnlockStorage = BiometricUnlockStorage as jest.Mocked -const mockAuthActions = authActions as jest.Mocked // Mock AuthenticatorAssertionResponse class MockAuthenticatorAssertionResponse { @@ -100,6 +97,7 @@ describe('useUnlockWithBiometricCredentialMutation', () => { // Setup default mocks mockBiometricUnlockStorage.get.mockResolvedValue({ credentialId: mockCredentialId, + transports: ['internal'], secretPayload: mockEncryptedPayload, }) @@ -107,15 +105,9 @@ describe('useUnlockWithBiometricCredentialMutation', () => { const mockPublicKeyCredential = new MockPublicKeyCredential(mockAuthResponse) mockCredentialsGet.mockResolvedValue(mockPublicKeyCredential) - mockAuthActions.trigger.mockReturnValue({ - type: 'AUTH_TRIGGER', - payload: { - type: AuthActionType.Unlock, - password: mockPassword, - }, - }) - - jest.clearAllMocks() + // Reset and configure mockUnlockWithPassword + mockUnlockWithPassword.mockReset() + mockUnlockWithPassword.mockResolvedValue(undefined) }) describe('successful unlock', () => { @@ -140,6 +132,7 @@ describe('useUnlockWithBiometricCredentialMutation', () => { { type: 'public-key', id: credentialIdBuffer, + transports: ['internal'], }, ], userVerification: 'required', @@ -148,11 +141,8 @@ describe('useUnlockWithBiometricCredentialMutation', () => { signal: expect.any(AbortSignal), }) - // 3. Should dispatch unlock action with the decrypted password - expect(mockAuthActions.trigger).toHaveBeenCalledWith({ - type: AuthActionType.Unlock, - password: mockPassword, - }) + // 3. Should call unlockWithPassword with the decrypted password + expect(mockUnlockWithPassword).toHaveBeenCalledWith({ password: mockPassword }) }) }) @@ -170,7 +160,7 @@ describe('useUnlockWithBiometricCredentialMutation', () => { expect(result.current.error?.message).toBe('No biometric unlock credential found') expect(mockCredentialsGet).not.toHaveBeenCalled() - expect(mockAuthActions.trigger).not.toHaveBeenCalled() + expect(mockUnlockWithPassword).not.toHaveBeenCalled() }) it('should throw error when biometric authentication fails', async () => { @@ -185,7 +175,7 @@ describe('useUnlockWithBiometricCredentialMutation', () => { }) expect(result.current.error?.message).toBe('Failed to create credential') - expect(mockAuthActions.trigger).not.toHaveBeenCalled() + expect(mockUnlockWithPassword).not.toHaveBeenCalled() }) it('should throw error when no user handle returned from authentication', async () => { @@ -202,7 +192,7 @@ describe('useUnlockWithBiometricCredentialMutation', () => { }) expect(result.current.error?.message).toBe('No user handle returned from biometric authentication') - expect(mockAuthActions.trigger).not.toHaveBeenCalled() + expect(mockUnlockWithPassword).not.toHaveBeenCalled() }) it('should throw error when password decryption fails', async () => { @@ -226,7 +216,7 @@ describe('useUnlockWithBiometricCredentialMutation', () => { }) expect(result.current.error?.message).toBe('Failed to decrypt password') - expect(mockAuthActions.trigger).not.toHaveBeenCalled() + expect(mockUnlockWithPassword).not.toHaveBeenCalled() }) it('should handle WebAuthn API errors', async () => { @@ -242,7 +232,7 @@ describe('useUnlockWithBiometricCredentialMutation', () => { }) expect(result.current.error).toBe(webAuthnError) - expect(mockAuthActions.trigger).not.toHaveBeenCalled() + expect(mockUnlockWithPassword).not.toHaveBeenCalled() }) it('should handle storage retrieval errors', async () => { @@ -259,7 +249,7 @@ describe('useUnlockWithBiometricCredentialMutation', () => { expect(result.current.error).toBe(storageError) expect(mockCredentialsGet).not.toHaveBeenCalled() - expect(mockAuthActions.trigger).not.toHaveBeenCalled() + expect(mockUnlockWithPassword).not.toHaveBeenCalled() }) }) diff --git a/apps/extension/src/app/features/biometricUnlock/useUnlockWithBiometricCredentialMutation.ts b/apps/extension/src/app/features/biometricUnlock/useUnlockWithBiometricCredentialMutation.ts index 4eac011d28f..b37d3357cb1 100644 --- a/apps/extension/src/app/features/biometricUnlock/useUnlockWithBiometricCredentialMutation.ts +++ b/apps/extension/src/app/features/biometricUnlock/useUnlockWithBiometricCredentialMutation.ts @@ -58,10 +58,10 @@ async function getPasswordFromBiometricCredential(abortSignal: AbortSignal): Pro throw new Error('No biometric unlock credential found') } - const { credentialId } = biometricUnlockCredential + const { credentialId, transports } = biometricUnlockCredential // Authenticate with WebAuthn using the stored credential and decrypt password - const { encryptionKey } = await authenticateWithBiometricCredential({ credentialId, abortSignal }) + const { encryptionKey } = await authenticateWithBiometricCredential({ credentialId, transports, abortSignal }) const password = await decryptPasswordFromBiometricData({ encryptionKey, biometricUnlockCredential }) return password } diff --git a/apps/extension/src/app/features/dappRequests/DappRequestContent.tsx b/apps/extension/src/app/features/dappRequests/DappRequestContent.tsx index b150cd8ff4a..06a1106bf53 100644 --- a/apps/extension/src/app/features/dappRequests/DappRequestContent.tsx +++ b/apps/extension/src/app/features/dappRequests/DappRequestContent.tsx @@ -7,26 +7,8 @@ import { useDappRequestQueueContext } from 'src/app/features/dappRequests/DappRe import { handleExternallySubmittedUniswapXOrder } from 'src/app/features/dappRequests/handleUniswapX' import { useIsDappRequestConfirming } from 'src/app/features/dappRequests/hooks' import { DappRequestStoreItem } from 'src/app/features/dappRequests/shared' -import { - DappRequest, - isBatchedSwapRequest, - isConnectionRequest, -} from 'src/app/features/dappRequests/types/DappRequestTypes' -import { - Anchor, - AnimatePresence, - Button, - Flex, - GetThemeValueForKey, - styled, - Text, - UniversalImage, - UniversalImageResizeMode, -} from 'ui/src' -import { Verified } from 'ui/src/components/icons' -import { borderRadii, iconSizes } from 'ui/src/theme' -import { DappIconPlaceholder } from 'uniswap/src/components/dapps/DappIconPlaceholder' -import { UNISWAP_WEB_HOSTNAME } from 'uniswap/src/constants/urls' +import { DappRequest, isBatchedSwapRequest } from 'src/app/features/dappRequests/types/DappRequestTypes' +import { AnimatePresence, Button, Flex, GetThemeValueForKey, styled, Text } from 'ui/src' import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { DappRequestType } from 'uniswap/src/features/dappRequests/types' @@ -35,18 +17,20 @@ import { hasSufficientFundsIncludingGas } from 'uniswap/src/features/gas/utils' import { useOnChainNativeCurrencyBalance } from 'uniswap/src/features/portfolio/api' import { TransactionTypeInfo } from 'uniswap/src/features/transactions/types/transactionDetails' import { extractNameFromUrl } from 'utilities/src/format/extractNameFromUrl' -import { formatDappURL } from 'utilities/src/format/urls' import { logger } from 'utilities/src/logger/logger' import { useEvent } from 'utilities/src/react/hooks' import { useThrottledCallback } from 'utilities/src/react/useThrottledCallback' import { MAX_HIDDEN_CALLS_BY_DEFAULT } from 'wallet/src/components/BatchedTransactions/BatchedTransactionDetails' +import { DappRequestHeader } from 'wallet/src/components/dappRequests/DappRequestHeader' import { WarningBox } from 'wallet/src/components/WarningBox/WarningBox' +import { DappVerificationStatus } from 'wallet/src/features/dappRequests/types' import { AddressFooter } from 'wallet/src/features/transactions/TransactionRequest/AddressFooter' import { NetworkFeeFooter } from 'wallet/src/features/transactions/TransactionRequest/NetworkFeeFooter' import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks' interface DappRequestHeaderProps { title: string + verificationStatus?: DappVerificationStatus headerIcon?: JSX.Element } @@ -57,9 +41,9 @@ interface DappRequestFooterProps { maybeCloseOnConfirm?: boolean onCancel?: (requestToConfirm?: DappRequestStoreItem, transactionTypeInfo?: TransactionTypeInfo) => void onConfirm?: (requestToCancel?: DappRequestStoreItem) => void - showAllNetworks?: boolean showNetworkCost?: boolean showSmartWalletActivation?: boolean + showAddressFooter?: boolean transactionGasFeeResult?: GasFeeResult isUniswapX?: boolean disableConfirm?: boolean @@ -96,26 +80,39 @@ export const AnimatedPane = styled(Flex, { export function DappRequestContent({ chainId, title, + verificationStatus, headerIcon, confirmText, connectedAccountAddress, maybeCloseOnConfirm, onCancel, onConfirm, - showAllNetworks, showNetworkCost, showSmartWalletActivation, transactionGasFeeResult, children, isUniswapX, disableConfirm, + showAddressFooter = true, contentHorizontalPadding = '$spacing12', }: PropsWithChildren): JSX.Element { - const { forwards, currentIndex } = useDappRequestQueueContext() + const { forwards, currentIndex, dappIconUrl, dappUrl } = useDappRequestQueueContext() + const hostname = extractNameFromUrl(dappUrl).toUpperCase() return ( <> - + + + {children} @@ -127,9 +124,9 @@ export function DappRequestContent({ connectedAccountAddress={connectedAccountAddress} isUniswapX={isUniswapX} maybeCloseOnConfirm={maybeCloseOnConfirm} - showAllNetworks={showAllNetworks} showNetworkCost={showNetworkCost} showSmartWalletActivation={showSmartWalletActivation} + showAddressFooter={showAddressFooter} transactionGasFeeResult={transactionGasFeeResult} disableConfirm={disableConfirm} onCancel={onCancel} @@ -139,48 +136,6 @@ export function DappRequestContent({ ) } -function DappRequestHeader({ headerIcon, title }: DappRequestHeaderProps): JSX.Element { - const { dappIconUrl, dappUrl, request } = useDappRequestQueueContext() - const hostname = extractNameFromUrl(dappUrl).toUpperCase() - const fallbackIcon = - const showVerified = - request && isConnectionRequest(request.dappRequest) && formatDappURL(dappUrl) === UNISWAP_WEB_HOSTNAME - - return ( - - - - {headerIcon || ( - - )} - - - - {title} - - - - - {formatDappURL(dappUrl)} - - {showVerified && } - - - - ) -} - const WINDOW_CLOSE_DELAY = 10 function DappRequestFooter({ @@ -192,6 +147,7 @@ function DappRequestFooter({ onConfirm, showNetworkCost, showSmartWalletActivation, + showAddressFooter, transactionGasFeeResult, isUniswapX, disableConfirm, @@ -275,7 +231,7 @@ function DappRequestFooter({ return ( <> - + {!hasSufficientGas && ( @@ -295,13 +251,15 @@ function DappRequestFooter({ showSmartWalletActivation={showSmartWalletActivation} /> )} - + {showAddressFooter && ( + + )} - + diff --git a/apps/extension/src/app/features/dappRequests/hooks/usePrepareAndSignSendCallsTransaction.ts b/apps/extension/src/app/features/dappRequests/hooks/usePrepareAndSignSendCallsTransaction.ts index c7cbb49bece..0ff33a6c29e 100644 --- a/apps/extension/src/app/features/dappRequests/hooks/usePrepareAndSignSendCallsTransaction.ts +++ b/apps/extension/src/app/features/dappRequests/hooks/usePrepareAndSignSendCallsTransaction.ts @@ -58,6 +58,7 @@ export function usePrepareAndSignSendCallsTransaction({ }) : [], smartContractDelegationAddress: UNISWAP_DELEGATION_ADDRESS, + // @ts-expect-error - TODO: no longer available in API types, verify if still needed walletAddress: account.address, }, }) diff --git a/apps/extension/src/app/features/dappRequests/requestContent/Connection/ConnectionRequestContent.tsx b/apps/extension/src/app/features/dappRequests/requestContent/Connection/ConnectionRequestContent.tsx index be130ca36d9..424e65d7324 100644 --- a/apps/extension/src/app/features/dappRequests/requestContent/Connection/ConnectionRequestContent.tsx +++ b/apps/extension/src/app/features/dappRequests/requestContent/Connection/ConnectionRequestContent.tsx @@ -1,27 +1,35 @@ import { useTranslation } from 'react-i18next' import { DappRequestContent } from 'src/app/features/dappRequests/DappRequestContent' -import { Flex, Text } from 'ui/src' +import { useDappRequestQueueContext } from 'src/app/features/dappRequests/DappRequestQueueContext' +import { AccountType } from 'uniswap/src/features/accounts/types' +import { DappConnectionContent } from 'wallet/src/components/dappRequests/DappConnectionContent' +import { useBlockaidVerification } from 'wallet/src/features/dappRequests/hooks/useBlockaidVerification' +import { useDappConnectionConfirmation } from 'wallet/src/features/dappRequests/hooks/useDappConnectionConfirmation' export function ConnectionRequestContent(): JSX.Element { const { t } = useTranslation() + const { currentAccount, dappUrl } = useDappRequestQueueContext() + const { verificationStatus } = useBlockaidVerification(dappUrl) + + const isViewOnly = currentAccount.type === AccountType.Readonly + const { confirmedWarning, setConfirmedWarning, disableConfirm } = useDappConnectionConfirmation({ + verificationStatus, + isViewOnly, + }) return ( - - - {t('dapp.request.connect.helptext')} - - + ) } diff --git a/apps/extension/src/app/features/dappRequests/requestContent/EthSend/EthSend.tsx b/apps/extension/src/app/features/dappRequests/requestContent/EthSend/EthSend.tsx index 7131e97a7f6..2c36e8b0657 100644 --- a/apps/extension/src/app/features/dappRequests/requestContent/EthSend/EthSend.tsx +++ b/apps/extension/src/app/features/dappRequests/requestContent/EthSend/EthSend.tsx @@ -1,3 +1,4 @@ +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { useCallback } from 'react' import { useDappLastChainId } from 'src/app/features/dapp/hooks' import { useDappRequestQueueContext } from 'src/app/features/dappRequests/DappRequestQueueContext' @@ -5,6 +6,7 @@ import { usePrepareAndSignEthSendTransaction } from 'src/app/features/dappReques import { ApproveRequestContent } from 'src/app/features/dappRequests/requestContent/EthSend/Approve/ApproveRequestContent' import { FallbackEthSendRequestContent } from 'src/app/features/dappRequests/requestContent/EthSend/FallbackEthSend/FallbackEthSend' import { LPRequestContent } from 'src/app/features/dappRequests/requestContent/EthSend/LP/LPRequestContent' +import { ParsedTransactionRequestContent } from 'src/app/features/dappRequests/requestContent/EthSend/ParsedTransaction/ParsedTransactionRequestContent' import { Permit2ApproveRequestContent } from 'src/app/features/dappRequests/requestContent/EthSend/Permit2Approve/Permit2ApproveRequestContent' import { SwapRequestContent } from 'src/app/features/dappRequests/requestContent/EthSend/Swap/SwapRequestContent' import { WrapRequestContent } from 'src/app/features/dappRequests/requestContent/EthSend/Wrap/WrapRequestContent' @@ -15,7 +17,9 @@ import { isPermit2ApproveRequest, isSwapRequest, isWrapRequest, + SendTransactionRequest, } from 'src/app/features/dappRequests/types/DappRequestTypes' +import { GasFeeResult } from 'uniswap/src/features/gas/types' import { TransactionTypeInfo } from 'uniswap/src/features/transactions/types/transactionDetails' import { logger } from 'utilities/src/logger/logger' import { ErrorBoundary } from 'wallet/src/components/ErrorBoundary/ErrorBoundary' @@ -28,6 +32,7 @@ export function EthSendRequestContent({ request }: EthSendRequestContentProps): const { dappRequest } = request const { dappUrl, currentAccount, onConfirm, onCancel } = useDappRequestQueueContext() const chainId = useDappLastChainId(dappUrl) + const blockaidTransactionScanning = useFeatureFlag(FeatureFlags.BlockaidTransactionScanning) const { gasFeeResult: transactionGasFeeResult, @@ -54,91 +59,147 @@ export function EthSendRequestContent({ request }: EthSendRequestContentProps): await onCancel(requestWithGasValues) }, [onCancel, requestWithGasValues]) - let content + // If Blockaid transaction scanning is enabled, try to use it for ALL transaction types + // If the API fails, the ErrorBoundary will catch it and fallback to specialized UIs + if (blockaidTransactionScanning) { + return ( + + } + onError={(error) => { + if (error) { + logger.error(error, { + tags: { file: 'EthSend', function: 'ErrorBoundary-Blockaid' }, + extra: { + dappRequest, + useSimulationResultUI: true, + }, + }) + } + }} + > + + + ) + } + + // If feature flag is disabled, use specialized UIs + const content = ( + + ) + + return ( + + } + onError={(error) => { + if (error) { + logger.error(error, { + tags: { file: 'EthSend', function: 'ErrorBoundary-Specialized' }, + extra: { + dappRequest, + useSimulationResultUI: false, + }, + }) + } + }} + > + {content} + + ) +} + +/** + * Fallback component that renders the appropriate specialized UI based on transaction type + * Used when simulation result UI is disabled or fails + */ +function SpecializedTransactionFallback({ + dappRequest, + transactionGasFeeResult, + onCancel, + onConfirm, +}: { + dappRequest: SendTransactionRequest + transactionGasFeeResult: GasFeeResult + onCancel: () => Promise + onConfirm: (transactionTypeInfo?: TransactionTypeInfo) => Promise +}): JSX.Element { switch (true) { case isSwapRequest(dappRequest): - content = ( + return ( ) - break case isPermit2ApproveRequest(dappRequest): - content = ( + return ( ) - break case isWrapRequest(dappRequest): - content = ( + return ( ) - break case isLPRequest(dappRequest): - content = ( + return ( ) - break case isApproveRequest(dappRequest): - content = ( + return ( ) - break default: - content = ( + return ( ) } - - return ( - - } - onError={(error) => { - if (error) { - logger.error(error, { - tags: { file: 'EthSend', function: 'ErrorBoundary' }, - extra: { - dappRequest, - }, - }) - } - }} - > - {content} - - ) } diff --git a/apps/extension/src/app/features/dappRequests/requestContent/EthSend/ParsedTransaction/ParsedTransactionRequestContent.tsx b/apps/extension/src/app/features/dappRequests/requestContent/EthSend/ParsedTransaction/ParsedTransactionRequestContent.tsx new file mode 100644 index 00000000000..ac63e20279a --- /dev/null +++ b/apps/extension/src/app/features/dappRequests/requestContent/EthSend/ParsedTransaction/ParsedTransactionRequestContent.tsx @@ -0,0 +1,74 @@ +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useDappLastChainId } from 'src/app/features/dapp/hooks' +import { DappRequestContent } from 'src/app/features/dappRequests/DappRequestContent' +import { useDappRequestQueueContext } from 'src/app/features/dappRequests/DappRequestQueueContext' +import { SendTransactionRequest } from 'src/app/features/dappRequests/types/DappRequestTypes' +import { GasFeeResult } from 'uniswap/src/features/gas/types' +import { useBooleanState } from 'utilities/src/react/useBooleanState' +import { DappTransactionScanningContent } from 'wallet/src/components/dappRequests/DappTransactionScanningContent' +import { TransactionRiskLevel } from 'wallet/src/features/dappRequests/types' +import { shouldDisableConfirm } from 'wallet/src/features/dappRequests/utils/riskUtils' + +interface ParsedTransactionRequestContentProps { + transactionGasFeeResult: GasFeeResult + dappRequest: SendTransactionRequest + onCancel: () => Promise + onConfirm: () => Promise +} + +/** + * Transaction request content with Blockaid security scanning + * Parses transaction data and displays it with asset transfers, security warnings, and detailed information + */ +export function ParsedTransactionRequestContent({ + dappRequest, + transactionGasFeeResult, + onCancel, + onConfirm, +}: ParsedTransactionRequestContentProps): JSX.Element | null { + const { t } = useTranslation() + const { dappUrl, currentAccount } = useDappRequestQueueContext() + const activeChain = useDappLastChainId(dappUrl) + const { value: confirmedRisk, setValue: setConfirmedRisk } = useBooleanState(false) + // Initialize with null to indicate scan hasn't completed yet + const [riskLevel, setRiskLevel] = useState(null) + + const { chainId: transactionChainId } = dappRequest.transaction + const chainId = transactionChainId ?? activeChain + + // If no valid chainId, throw so that we fall back to the legacy UI + if (!chainId) { + throw new Error('No valid chainId available for transaction scanning') + } + + const disableConfirm = shouldDisableConfirm({ + riskLevel, + confirmedRisk, + hasGasFee: !!transactionGasFeeResult.value, + }) + + return ( + + + + ) +} diff --git a/apps/extension/src/app/features/dappRequests/requestContent/EthSend/Swap/SwapRequestContent.tsx b/apps/extension/src/app/features/dappRequests/requestContent/EthSend/Swap/SwapRequestContent.tsx index 4aec47e93aa..05578f8627c 100644 --- a/apps/extension/src/app/features/dappRequests/requestContent/EthSend/Swap/SwapRequestContent.tsx +++ b/apps/extension/src/app/features/dappRequests/requestContent/EthSend/Swap/SwapRequestContent.tsx @@ -2,7 +2,6 @@ import { useDappLastChainId } from 'src/app/features/dapp/hooks' import { useDappRequestQueueContext } from 'src/app/features/dappRequests/DappRequestQueueContext' import { SwapDisplay } from 'src/app/features/dappRequests/requestContent/EthSend/Swap/SwapDisplay' import { formatUnits, useSwapDetails } from 'src/app/features/dappRequests/requestContent/EthSend/Swap/utils' -import { UniswapXSwapRequest } from 'src/app/features/dappRequests/types/Permit2Types' import { UniversalRouterCall } from 'src/app/features/dappRequests/types/UniversalRouterTypes' import { DEFAULT_NATIVE_ADDRESS, DEFAULT_NATIVE_ADDRESS_LEGACY } from 'uniswap/src/features/chains/evm/defaults' import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' @@ -13,6 +12,7 @@ import { useCurrencyInfo, useNativeCurrencyInfo } from 'uniswap/src/features/tok import { TransactionType, TransactionTypeInfo } from 'uniswap/src/features/transactions/types/transactionDetails' import { buildCurrencyId } from 'uniswap/src/utils/currencyId' import { assert } from 'utilities/src/errors' +import { UniswapXSwapRequest } from 'wallet/src/components/dappRequests/types/Permit2Types' function getTransactionTypeInfo({ inputCurrencyInfo, diff --git a/apps/extension/src/app/features/dappRequests/requestContent/PersonalSign/PersonalSignRequestContent.tsx b/apps/extension/src/app/features/dappRequests/requestContent/PersonalSign/PersonalSignRequestContent.tsx index 82183fc49fe..fe179239aa6 100644 --- a/apps/extension/src/app/features/dappRequests/requestContent/PersonalSign/PersonalSignRequestContent.tsx +++ b/apps/extension/src/app/features/dappRequests/requestContent/PersonalSign/PersonalSignRequestContent.tsx @@ -1,11 +1,21 @@ import { toUtf8String } from '@ethersproject/strings' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' +import { useDappLastChainId } from 'src/app/features/dapp/hooks' import { DappRequestContent } from 'src/app/features/dappRequests/DappRequestContent' +import { useDappRequestQueueContext } from 'src/app/features/dappRequests/DappRequestQueueContext' import { SignMessageRequest } from 'src/app/features/dappRequests/types/DappRequestTypes' import { Flex, IconButton, Text, Tooltip } from 'ui/src' import { AlertTriangleFilled, Code, StickyNoteTextSquare } from 'ui/src/components/icons' +import { zIndexes } from 'ui/src/theme' +import { EthMethod } from 'uniswap/src/features/dappRequests/types' +import { logger } from 'utilities/src/logger/logger' import { containsNonPrintableChars } from 'utilities/src/primitives/string' +import { useBooleanState } from 'utilities/src/react/useBooleanState' +import { DappPersonalSignContent } from 'wallet/src/components/dappRequests/DappPersonalSignContent' +import { TransactionRiskLevel } from 'wallet/src/features/dappRequests/types' +import { shouldDisableConfirm } from 'wallet/src/features/dappRequests/utils/riskUtils' enum ViewEncoding { UTF8 = 0, @@ -16,26 +26,111 @@ interface PersonalSignRequestProps { } export function PersonalSignRequestContent({ dappRequest }: PersonalSignRequestProps): JSX.Element | null { + const blockaidTransactionScanning = useFeatureFlag(FeatureFlags.BlockaidTransactionScanning) + + // Decode message to UTF-8 + const hexMessage = dappRequest.messageHex + const [utf8Message, setUtf8Message] = useState() + + useEffect(() => { + try { + const decodedMessage = toUtf8String(hexMessage) + setUtf8Message(decodedMessage) + } catch { + // If the message is not valid UTF-8, we'll show the hex message instead + setUtf8Message(undefined) + } + }, [hexMessage]) + + if (blockaidTransactionScanning) { + return + } + + return +} + +/** + * Implementation with Blockaid scanning + */ +function PersonalSignRequestContentWithScanning({ + dappRequest, + utf8Message, +}: { + dappRequest: SignMessageRequest + utf8Message: string | undefined +}): JSX.Element { + const { t } = useTranslation() + const { dappUrl, currentAccount } = useDappRequestQueueContext() + const activeChain = useDappLastChainId(dappUrl) + const { value: confirmedRisk, setValue: setConfirmedRisk } = useBooleanState(false) + // Initialize with null to indicate scan hasn't completed yet + const [riskLevel, setRiskLevel] = useState(null) + + const hexMessage = dappRequest.messageHex + const isDecoded = Boolean(utf8Message && !containsNonPrintableChars(utf8Message)) + const message = (isDecoded ? utf8Message : hexMessage) || hexMessage + const hasLoggedError = useRef(false) + + if (!activeChain) { + if (!hasLoggedError.current) { + logger.error(new Error('No active chain found'), { + tags: { file: 'PersonalSignRequestContent', function: 'PersonalSignRequestContentWithScanning' }, + }) + hasLoggedError.current = true + } + return + } + + const disableConfirm = shouldDisableConfirm({ riskLevel, confirmedRisk }) + + return ( + + + + ) +} + +/** + * Legacy implementation (existing behavior when feature flag is off) + */ +function PersonalSignRequestContentLegacy({ + dappRequest, + utf8Message, +}: { + dappRequest: SignMessageRequest + utf8Message: string | undefined +}): JSX.Element { const { t } = useTranslation() const [viewEncoding, setViewEncoding] = useState(ViewEncoding.UTF8) - const [utf8Message, setUtf8Message] = useState() const toggleViewEncoding = (): void => setViewEncoding(viewEncoding === ViewEncoding.UTF8 ? ViewEncoding.HEX : ViewEncoding.UTF8) const hexMessage = dappRequest.messageHex const containsUnrenderableCharacters = !utf8Message || containsNonPrintableChars(utf8Message) + useEffect(() => { - try { - const decodedMessage = toUtf8String(hexMessage) - setUtf8Message(decodedMessage) - } catch { - // If the message is not valid UTF-8, we'll show the hex message instead (e.g. Polymark claim deposit message ) + if (!utf8Message) { setViewEncoding(ViewEncoding.HEX) - setUtf8Message(undefined) } - }, [hexMessage]) + }, [utf8Message]) const [isScrollable, setIsScrollable] = useState(false) const messageRef = useRef(null) @@ -96,7 +191,7 @@ export function PersonalSignRequestContent({ dappRequest }: PersonalSignRequestP /> - + {viewEncoding === ViewEncoding.UTF8 diff --git a/apps/extension/src/app/features/dappRequests/requestContent/SendCalls/SendCallsRequestContent.tsx b/apps/extension/src/app/features/dappRequests/requestContent/SendCalls/SendCallsRequestContent.tsx index 3245fde4e74..db74d62bf53 100644 --- a/apps/extension/src/app/features/dappRequests/requestContent/SendCalls/SendCallsRequestContent.tsx +++ b/apps/extension/src/app/features/dappRequests/requestContent/SendCalls/SendCallsRequestContent.tsx @@ -1,4 +1,5 @@ -import { useCallback, useMemo } from 'react' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' +import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { useDappLastChainId } from 'src/app/features/dapp/hooks' import { DappRequestContent } from 'src/app/features/dappRequests/DappRequestContent' @@ -12,9 +13,14 @@ import { ParsedCall, SendCallsRequest, } from 'src/app/features/dappRequests/types/DappRequestTypes' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { GasFeeResult } from 'uniswap/src/features/gas/types' import { TransactionType, TransactionTypeInfo } from 'uniswap/src/features/transactions/types/transactionDetails' +import { useBooleanState } from 'utilities/src/react/useBooleanState' import { BatchedRequestDetailsContent } from 'wallet/src/components/BatchedTransactions/BatchedTransactionDetails' +import { DappSendCallsScanningContent } from 'wallet/src/components/dappRequests/DappSendCallsScanningContent' +import { TransactionRiskLevel } from 'wallet/src/features/dappRequests/types' +import { shouldDisableConfirm } from 'wallet/src/features/dappRequests/utils/riskUtils' interface SendCallsRequestContentProps { dappRequest: SendCallsRequest @@ -24,7 +30,60 @@ interface SendCallsRequestContentProps { onCancel: () => Promise } -function SendCallsRequestContent({ +/** + * Implementation with Blockaid scanning + */ +function SendCallsRequestContentWithScanning({ + dappRequest, + chainId, + transactionGasFeeResult, + showSmartWalletActivation, + onConfirm, + onCancel, +}: SendCallsRequestContentProps & { chainId: UniverseChainId }): JSX.Element { + const { t } = useTranslation() + const { dappUrl, currentAccount } = useDappRequestQueueContext() + const { value: confirmedRisk, setValue: setConfirmedRisk } = useBooleanState(false) + // Initialize with null to indicate scan hasn't completed yet + const [riskLevel, setRiskLevel] = useState(null) + + const disableConfirm = shouldDisableConfirm({ + riskLevel, + confirmedRisk, + hasGasFee: !!transactionGasFeeResult.value, + }) + + return ( + onConfirm()} + showAddressFooter={false} + > + + + ) +} + +/** + * Legacy implementation (existing behavior when feature flag is off) + */ +function SendCallsRequestContentLegacy({ dappRequest, transactionGasFeeResult, showSmartWalletActivation, @@ -56,6 +115,7 @@ function SendCallsRequestContent({ export function SendCallsRequestHandler({ request }: { request: DappRequestStoreItemForSendCallsTxn }): JSX.Element { const { dappUrl, currentAccount, onConfirm, onCancel } = useDappRequestQueueContext() const chainId = useDappLastChainId(dappUrl) ?? request.dappInfo?.lastChainId + const blockaidTransactionScanning = useFeatureFlag(FeatureFlags.BlockaidTransactionScanning) const { dappRequest } = request @@ -92,7 +152,16 @@ export function SendCallsRequestHandler({ request }: { request: DappRequestStore await onCancel(request) }, [onCancel, request]) - return parsedSwapCalldata ? ( + return blockaidTransactionScanning && chainId ? ( + + ) : parsedSwapCalldata ? ( ) : ( - ) } diff --git a/apps/extension/src/app/features/dappRequests/requestContent/SignTypeData/NonStandardTypedDataRequestContent.tsx b/apps/extension/src/app/features/dappRequests/requestContent/SignTypeData/NonStandardTypedDataRequestContent.tsx index 36ae109cf85..37eff930616 100644 --- a/apps/extension/src/app/features/dappRequests/requestContent/SignTypeData/NonStandardTypedDataRequestContent.tsx +++ b/apps/extension/src/app/features/dappRequests/requestContent/SignTypeData/NonStandardTypedDataRequestContent.tsx @@ -2,10 +2,7 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' import { DappRequestContent } from 'src/app/features/dappRequests/DappRequestContent' import { SignTypedDataRequest } from 'src/app/features/dappRequests/types/DappRequestTypes' -import { Flex, Separator, Text } from 'ui/src' -import { Clear, Signature } from 'ui/src/components/icons' -import { InlineWarningCard } from 'uniswap/src/components/InlineWarningCard/InlineWarningCard' -import { WarningSeverity } from 'uniswap/src/components/modals/WarningModal/types' +import { NonStandardTypedDataContent } from 'wallet/src/components/dappRequests/SignTypedData/NonStandardTypedDataContent' interface NonStandardTypedDataRequestContentProps { dappRequest: SignTypedDataRequest @@ -17,8 +14,6 @@ export function NonStandardTypedDataRequestContent({ const { t } = useTranslation() const [checked, setChecked] = useState(false) - const hasMessageToShow = !!dappRequest.typedData - return ( - - - - - - {t('dapp.request.signature.decodeError')} - - - {hasMessageToShow && } - {hasMessageToShow && ( - - - - - {t('common.message')} - - - - {dappRequest.typedData} - - - )} - - - + ) } diff --git a/apps/extension/src/app/features/dappRequests/requestContent/SignTypeData/Permit2/Permit2RequestContent.tsx b/apps/extension/src/app/features/dappRequests/requestContent/SignTypeData/Permit2/Permit2RequestContent.tsx deleted file mode 100644 index 346fb612654..00000000000 --- a/apps/extension/src/app/features/dappRequests/requestContent/SignTypeData/Permit2/Permit2RequestContent.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { useState } from 'react' -import { useTranslation } from 'react-i18next' -import { DappRequestContent } from 'src/app/features/dappRequests/DappRequestContent' -import { DomainContent } from 'src/app/features/dappRequests/requestContent/SignTypeData/DomainContent' -import { MaybeExplorerLinkedAddress } from 'src/app/features/dappRequests/requestContent/SignTypeData/MaybeExplorerLinkedAddress' -import { SignTypedDataRequest } from 'src/app/features/dappRequests/types/DappRequestTypes' -import { Flex, Text, TouchableArea } from 'ui/src' -import { RotatableChevron } from 'ui/src/components/icons' -import { toSupportedChainId } from 'uniswap/src/features/chains/utils' -import { ExplorerDataType, getExplorerLink } from 'uniswap/src/utils/linking' - -interface Permit2RequestProps { - dappRequest: SignTypedDataRequest -} - -export function Permit2RequestContent({ dappRequest }: Permit2RequestProps): JSX.Element | null { - const { t } = useTranslation() - - const parsedTypedData = JSON.parse(dappRequest.typedData) - const { name, chainId: domainChainId, verifyingContract } = parsedTypedData?.domain || {} - const chainId = toSupportedChainId(domainChainId) - - const { token: address, amount, expiration, nonce } = parsedTypedData?.message?.details || {} - const { spender, sigDeadline } = parsedTypedData?.message || {} - const [open, setOpen] = useState(false) - const toggleOpen = (): void => setOpen(!open) - - const spenderLink = chainId ? getExplorerLink({ chainId, data: spender, type: ExplorerDataType.ADDRESS }) : undefined - const tokenLink = chainId ? getExplorerLink({ chainId, data: address, type: ExplorerDataType.TOKEN }) : undefined - - return ( - - - - - {t('dapp.request.permit2.description')} - - - - - - {open && ( - <> - - - - token - - - - - - amount - - - {amount} - - - - - expiration - - - {expiration} - - - - - nonce - - - {nonce} - - - - - spender - - - - - - signature deadline - - - {sigDeadline} - - - - )} - - - ) -} diff --git a/apps/extension/src/app/features/dappRequests/saga.ts b/apps/extension/src/app/features/dappRequests/saga.ts index 7a511bca103..1c2a07a605a 100644 --- a/apps/extension/src/app/features/dappRequests/saga.ts +++ b/apps/extension/src/app/features/dappRequests/saga.ts @@ -59,7 +59,7 @@ import { extractBaseUrl } from 'utilities/src/format/urls' import { logger } from 'utilities/src/logger/logger' import { getCallsStatusHelper } from 'wallet/src/features/batchedTransactions/eip5792Utils' import { addBatchedTransaction } from 'wallet/src/features/batchedTransactions/slice' -import { generateBatchId, getCapabilitiesCore } from 'wallet/src/features/batchedTransactions/utils' +import { generateBatchId, getCapabilitiesResponse } from 'wallet/src/features/batchedTransactions/utils' import { Call } from 'wallet/src/features/dappRequests/types' import { ExecuteTransactionParams, @@ -530,7 +530,7 @@ export function* handleGetCapabilities(request: GetCapabilitiesRequest, senderTa const hasSmartWalletConsent = yield* select(selectHasSmartWalletConsent, request.address) const chainIds = request.chainIds?.map(hexadecimalStringToInt) ?? enabledChains.map((chain) => chain.valueOf()) - const response = yield* call(getCapabilitiesCore, { + const response = yield* call(getCapabilitiesResponse, { request, chainIds, hasSmartWalletConsent, diff --git a/apps/extension/src/app/features/dappRequests/types/EthersTypes.ts b/apps/extension/src/app/features/dappRequests/types/EthersTypes.ts index df5e5d1d9dc..37307d7e8af 100644 --- a/apps/extension/src/app/features/dappRequests/types/EthersTypes.ts +++ b/apps/extension/src/app/features/dappRequests/types/EthersTypes.ts @@ -44,7 +44,7 @@ export const EthersTransactionRequestSchema = z.object({ data: BytesLikeSchema.optional(), value: BigNumberishSchema.optional(), chainId: HexadecimalNumberSchema.optional(), - type: z.number().optional(), + type: z.union([z.number(), HexadecimalNumberSchema]).optional(), accessList: AccessListishSchema.optional(), maxPriorityFeePerGas: BigNumberishSchema.optional(), maxFeePerGas: BigNumberishSchema.optional(), diff --git a/apps/extension/src/app/features/home/PortfolioHeader.tsx b/apps/extension/src/app/features/home/PortfolioHeader.tsx index b951fd5c50a..1a13b6b6b9f 100644 --- a/apps/extension/src/app/features/home/PortfolioHeader.tsx +++ b/apps/extension/src/app/features/home/PortfolioHeader.tsx @@ -189,7 +189,7 @@ export const PortfolioHeader = memo(function _PortfolioHeader({ address }: Portf borderColor="$surface2" borderRadius="$rounded20" borderWidth="$spacing1" - disableRemoveScroll={false} + enableRemoveScroll={true} zIndex="$default" {...animationPresets.fadeInDownOutUp} shadowColor="$shadowColor" diff --git a/apps/extension/src/app/features/home/TokenBalanceList.tsx b/apps/extension/src/app/features/home/TokenBalanceList.tsx index b29a53a4a2c..ebbed006847 100644 --- a/apps/extension/src/app/features/home/TokenBalanceList.tsx +++ b/apps/extension/src/app/features/home/TokenBalanceList.tsx @@ -1,9 +1,13 @@ -import { memo } from 'react' +import { Currency } from '@uniswap/sdk-core' +import { memo, useState } from 'react' import { useInterfaceBuyNavigator } from 'src/app/features/for/utils' import { AppRoutes } from 'src/app/navigation/constants' import { navigate } from 'src/app/navigation/state' import { TokenBalanceListWeb } from 'uniswap/src/components/portfolio/TokenBalanceListWeb' +import { ReportTokenIssueModal } from 'uniswap/src/components/reporting/ReportTokenIssueModal' import { ElementName } from 'uniswap/src/features/telemetry/constants' +import { useEvent } from 'utilities/src/react/hooks' +import { useBooleanState } from 'utilities/src/react/useBooleanState' import { usePortfolioEmptyStateBackground } from 'wallet/src/components/portfolio/empty' export const ExtensionTokenBalanceList = memo(function _ExtensionTokenBalanceList({ @@ -15,13 +19,35 @@ export const ExtensionTokenBalanceList = memo(function _ExtensionTokenBalanceLis navigate(`/${AppRoutes.Receive}`) } const onPressBuy = useInterfaceBuyNavigator(ElementName.EmptyStateBuy) + + const { value: isReportTokenModalOpen, setTrue: openModal, setFalse: closeReportTokenModal } = useBooleanState(false) + const [reportTokenCurrency, setReportTokenCurrency] = useState(undefined) + const [isMarkedSpam, setIsMarkedSpam] = useState>(undefined) + + const openReportTokenModal = useEvent((currency: Currency, isMarkedSpam: Maybe) => { + setReportTokenCurrency(currency) + setIsMarkedSpam(isMarkedSpam) + openModal() + }) + const backgroundImageWrapperCallback = usePortfolioEmptyStateBackground() return ( - + <> + + {reportTokenCurrency && ( + + )} + ) }) diff --git a/apps/extension/src/app/features/home/introCards/HomeIntroCardStack.tsx b/apps/extension/src/app/features/home/introCards/HomeIntroCardStack.tsx index 119a661a5d0..45723370f88 100644 --- a/apps/extension/src/app/features/home/introCards/HomeIntroCardStack.tsx +++ b/apps/extension/src/app/features/home/introCards/HomeIntroCardStack.tsx @@ -1,8 +1,10 @@ -import { useCallback } from 'react' +import { useCallback, useState } from 'react' import { AppRoutes, SettingsRoutes, UnitagClaimRoutes } from 'src/app/navigation/constants' import { focusOrCreateUnitagTab, useExtensionNavigation } from 'src/app/navigation/utils' import { Flex } from 'ui/src' +import { MonadAnnouncementModal } from 'uniswap/src/components/notifications/MonadAnnouncementModal' import { AccountType } from 'uniswap/src/features/accounts/types' +import { useEvent } from 'utilities/src/react/hooks' import { IntroCardStack } from 'wallet/src/components/introCards/IntroCardStack' import { useSharedIntroCards } from 'wallet/src/components/introCards/useSharedIntroCards' import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks' @@ -11,6 +13,7 @@ export function HomeIntroCardStack(): JSX.Element | null { const activeAccount = useActiveAccountWithThrow() const isSignerAccount = activeAccount.type === AccountType.SignerMnemonic const { navigateTo } = useExtensionNavigation() + const [isMonadModalOpen, setIsMonadModalOpen] = useState(false) const navigateToUnitagClaim = useCallback(async () => { await focusOrCreateUnitagTab(activeAccount.address, UnitagClaimRoutes.ClaimIntro) @@ -20,10 +23,16 @@ export function HomeIntroCardStack(): JSX.Element | null { navigateTo(`/${AppRoutes.Settings}/${SettingsRoutes.BackupRecoveryPhrase}`) }, [navigateTo]) + const handleMonadExplorePress = useEvent(() => { + window.open('https://app.uniswap.org/explore/tokens/monad', '_blank') + setIsMonadModalOpen(false) + }) + const { cards } = useSharedIntroCards({ navigateToUnitagClaim, navigateToUnitagIntro: navigateToUnitagClaim, // No need to differentiate for extension navigateToBackupFlow, + onMonadAnnouncementPress: () => setIsMonadModalOpen(true), }) // Don't show cards if there are none @@ -33,8 +42,17 @@ export function HomeIntroCardStack(): JSX.Element | null { } return ( - - - + <> + + + + {isMonadModalOpen && ( + setIsMonadModalOpen(false)} + onExplorePress={handleMonadExplorePress} + /> + )} + ) } diff --git a/apps/extension/src/app/features/onboarding/import/ImportMnemonic.tsx b/apps/extension/src/app/features/onboarding/import/ImportMnemonic.tsx index 0378892eb3a..2c89cc0cad8 100644 --- a/apps/extension/src/app/features/onboarding/import/ImportMnemonic.tsx +++ b/apps/extension/src/app/features/onboarding/import/ImportMnemonic.tsx @@ -234,7 +234,9 @@ export function ImportMnemonic(): JSX.Element { (inputRefs[index] = ref)} + ref={(ref) => { + inputRefs[index] = ref + }} handleBlur={handleBlur} handleChange={handleChange} handleKeyPress={handleKeyPress} diff --git a/apps/extension/src/app/features/onboarding/import/SelectWallets.tsx b/apps/extension/src/app/features/onboarding/import/SelectWallets.tsx index 3c8597824a0..3f12f876606 100644 --- a/apps/extension/src/app/features/onboarding/import/SelectWallets.tsx +++ b/apps/extension/src/app/features/onboarding/import/SelectWallets.tsx @@ -8,7 +8,7 @@ import { useOnboardingSteps } from 'src/app/features/onboarding/OnboardingSteps' import { useSubmitOnEnter } from 'src/app/features/onboarding/utils' import { Flex, ScrollView, SpinningLoader, Square, Text, Tooltip, TouchableArea } from 'ui/src' import { WalletFilled } from 'ui/src/components/icons' -import { iconSizes } from 'ui/src/theme' +import { iconSizes, zIndexes } from 'ui/src/theme' import { uniswapUrls } from 'uniswap/src/constants/urls' import Trace from 'uniswap/src/features/telemetry/Trace' import { ExtensionOnboardingFlow, ExtensionOnboardingScreens } from 'uniswap/src/types/screens/extension' @@ -132,7 +132,7 @@ function SmartWalletTooltip(): JSX.Element | undefined { - + diff --git a/apps/extension/src/app/features/onboarding/scan/OTPInput.tsx b/apps/extension/src/app/features/onboarding/scan/OTPInput.tsx index 4075ff3544d..9d3f1093616 100644 --- a/apps/extension/src/app/features/onboarding/scan/OTPInput.tsx +++ b/apps/extension/src/app/features/onboarding/scan/OTPInput.tsx @@ -51,7 +51,7 @@ export function OTPInput(): JSX.Element { const [failedAttemptCount, setFailedAttemptCount] = useState(0) const [characterSequence, setCharacterSequence] = useState(INITIAL_CHARACTER_SEQUENCE) - const inputRefs = useRef[]>([]) + const inputRefs = useRef[]>([]) inputRefs.current = new Array(6).fill(null).map((_, i) => inputRefs.current[i] || createRef()) // Add all accounts from mnemonic. diff --git a/apps/extension/src/app/features/receive/__snapshots__/ReceiveScreen.test.tsx.snap b/apps/extension/src/app/features/receive/__snapshots__/ReceiveScreen.test.tsx.snap index 4fe7b9ea7e4..d178a5ab606 100644 --- a/apps/extension/src/app/features/receive/__snapshots__/ReceiveScreen.test.tsx.snap +++ b/apps/extension/src/app/features/receive/__snapshots__/ReceiveScreen.test.tsx.snap @@ -20,8 +20,10 @@ exports[`ReceiveScreen renders without error 1`] = ` >
- Ethereum address + Wallet address
@@ -6090,6 +6100,19 @@ exports[`ReceiveScreen renders without error 1`] = ` />
+ + + + + , "container":
- Ethereum address + Wallet address
@@ -12236,4 +12269,4 @@ exports[`ReceiveScreen renders without error 1`] = ` }, "unmount": [Function], } -`; +`; \ No newline at end of file diff --git a/apps/extension/src/app/features/send/SendFlow.tsx b/apps/extension/src/app/features/send/SendFlow.tsx index 5136b6f464f..716c9cd81ff 100644 --- a/apps/extension/src/app/features/send/SendFlow.tsx +++ b/apps/extension/src/app/features/send/SendFlow.tsx @@ -6,7 +6,9 @@ import { useExtensionNavigation } from 'src/app/navigation/utils' import { Flex } from 'ui/src' import { X } from 'ui/src/components/icons' import { ModalName } from 'uniswap/src/features/telemetry/constants' +import { TransactionSettingsStoreContextProvider } from 'uniswap/src/features/transactions/components/settings/stores/transactionSettingsStore/TransactionSettingsStoreContextProvider' import { TransactionModal } from 'uniswap/src/features/transactions/components/TransactionModal/TransactionModal' +import { SwapFormStoreContextProvider } from 'uniswap/src/features/transactions/swap/stores/swapFormStore/SwapFormStoreContextProvider' import { SendContextProvider } from 'wallet/src/features/transactions/contexts/SendContext' export function SendFlow(): JSX.Element { @@ -15,16 +17,20 @@ export function SendFlow(): JSX.Element { return ( null}> - - - - - - - - - - + + + + + + + + + + + + + + ) } diff --git a/apps/extension/src/app/features/settings/DeviceAccessScreen.tsx b/apps/extension/src/app/features/settings/DeviceAccessScreen.tsx index c9b6d70c21b..c739b0cbe06 100644 --- a/apps/extension/src/app/features/settings/DeviceAccessScreen.tsx +++ b/apps/extension/src/app/features/settings/DeviceAccessScreen.tsx @@ -48,6 +48,7 @@ export function DeviceAccessScreen(): JSX.Element { const { flowState, + oldPassword, startPasswordReset, closeModal, onPasswordModalNext, @@ -98,6 +99,7 @@ export function DeviceAccessScreen(): JSX.Element { return ( closeModal(PasswordResetFlowState.EnterNewPassword)} /> diff --git a/apps/extension/src/app/features/settings/SettingsDropdown.tsx b/apps/extension/src/app/features/settings/SettingsDropdown.tsx index 8df26d5fe71..bcee8207b0f 100644 --- a/apps/extension/src/app/features/settings/SettingsDropdown.tsx +++ b/apps/extension/src/app/features/settings/SettingsDropdown.tsx @@ -46,7 +46,7 @@ export function SettingsDropdown({ selected, items, disableDropdown, onSelect }: /> - + setIsPortfolioBalanceModalOpen(true)} /> {isSmartWalletEnabled ? ( diff --git a/apps/extension/src/app/features/settings/password/ChangePasswordForm.test.tsx b/apps/extension/src/app/features/settings/password/ChangePasswordForm.test.tsx new file mode 100644 index 00000000000..f4721fb4899 --- /dev/null +++ b/apps/extension/src/app/features/settings/password/ChangePasswordForm.test.tsx @@ -0,0 +1,166 @@ +import { act, fireEvent, waitFor } from '@testing-library/react' +import { ChangePasswordForm } from 'src/app/features/settings/password/ChangePasswordForm' +import { cleanup, render, screen } from 'src/test/test-utils' +import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring' + +// Mock the Keyring +jest.mock('wallet/src/features/wallet/Keyring/Keyring', () => ({ + Keyring: { + changePassword: jest.fn().mockResolvedValue(undefined), + }, +})) + +// Mock analytics +jest.mock('uniswap/src/features/telemetry/send', () => ({ + sendAnalyticsEvent: jest.fn(), +})) + +const mockChangePassword = Keyring.changePassword as jest.MockedFunction + +describe('ChangePasswordForm', () => { + const mockOnNext = jest.fn() + const oldPassword = 'MyOldPassword123!' + + beforeEach(() => { + jest.clearAllMocks() + mockChangePassword.mockClear() + }) + + afterEach(() => { + cleanup() + }) + + it('renders without error', () => { + const tree = render() + expect(tree).toMatchSnapshot() + }) + + it('renders password input fields', () => { + render() + + // Check for translated placeholders + expect(screen.getByPlaceholderText('New password')).toBeDefined() + expect(screen.getByPlaceholderText('Confirm password')).toBeDefined() + }) + + it('renders continue button', () => { + render() + + expect(screen.getByText('Continue')).toBeDefined() + }) + + it('shows error when new password matches old password', async () => { + render() + + const newPasswordInput = screen.getByPlaceholderText('New password') + const confirmPasswordInput = screen.getByPlaceholderText('Confirm password') + + // Type the same password as the old password + act(() => { + fireEvent.change(newPasswordInput, { target: { value: oldPassword } }) + fireEvent.change(confirmPasswordInput, { target: { value: oldPassword } }) + }) + + // Wait for error to appear + await waitFor(() => { + const errorText = screen.getByText('New password must be different from current password') + expect(errorText).toBeDefined() + }) + }) + + it('clears error when password changes to be different', async () => { + render() + + const newPasswordInput = screen.getByPlaceholderText('New password') + const confirmPasswordInput = screen.getByPlaceholderText('Confirm password') + + // First, type the same password as old password + act(() => { + fireEvent.change(newPasswordInput, { target: { value: oldPassword } }) + fireEvent.change(confirmPasswordInput, { target: { value: oldPassword } }) + }) + + // Wait for error to appear + await waitFor(() => { + expect(screen.getByText('New password must be different from current password')).toBeDefined() + }) + + // Clear and type a different password + act(() => { + fireEvent.change(newPasswordInput, { target: { value: 'DifferentPassword789!' } }) + fireEvent.change(confirmPasswordInput, { target: { value: 'DifferentPassword789!' } }) + }) + + // Error should be cleared + await waitFor(() => { + expect(screen.queryByText('New password must be different from current password')).toBeNull() + }) + }) + + it('does not call onNext when passwords match old password', async () => { + render() + + const newPasswordInput = screen.getByPlaceholderText('New password') + const confirmPasswordInput = screen.getByPlaceholderText('Confirm password') + const submitButton = screen.getByText('Continue') + + // Type the same password as the old password + act(() => { + fireEvent.change(newPasswordInput, { target: { value: oldPassword } }) + fireEvent.change(confirmPasswordInput, { target: { value: oldPassword } }) + }) + + // Try to submit + await act(async () => { + fireEvent.click(submitButton) + }) + + expect(mockOnNext).not.toHaveBeenCalled() + expect(mockChangePassword).not.toHaveBeenCalled() + }) + + it('calls onNext and changePassword when passwords are different and valid', async () => { + render() + + const newPasswordInput = screen.getByPlaceholderText('New password') + const confirmPasswordInput = screen.getByPlaceholderText('Confirm password') + const submitButton = screen.getByText('Continue') + + const newPassword = 'MyNewStrongPassword456!' + + // Type a different password + act(() => { + fireEvent.change(newPasswordInput, { target: { value: newPassword } }) + fireEvent.change(confirmPasswordInput, { target: { value: newPassword } }) + }) + + // Submit the form + await act(async () => { + fireEvent.click(submitButton) + }) + + await waitFor(() => { + expect(mockChangePassword).toHaveBeenCalledWith(newPassword) + expect(mockOnNext).toHaveBeenCalledWith(newPassword) + }) + }) + + it('handles undefined oldPassword gracefully', async () => { + render() + + const newPasswordInput = screen.getByPlaceholderText('New password') + const confirmPasswordInput = screen.getByPlaceholderText('Confirm password') + + const newPassword = 'AnyPassword123!' + + // Type any password - should not show "same password" error since oldPassword is undefined + act(() => { + fireEvent.change(newPasswordInput, { target: { value: newPassword } }) + fireEvent.change(confirmPasswordInput, { target: { value: newPassword } }) + }) + + await waitFor(() => { + expect(screen.queryByText('New password must be different from current password')).toBeNull() + }) + }) +}) diff --git a/apps/extension/src/app/features/settings/password/ChangePasswordForm.tsx b/apps/extension/src/app/features/settings/password/ChangePasswordForm.tsx index c03b41a1cac..d82b56ba129 100644 --- a/apps/extension/src/app/features/settings/password/ChangePasswordForm.tsx +++ b/apps/extension/src/app/features/settings/password/ChangePasswordForm.tsx @@ -1,13 +1,19 @@ -import { useCallback } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { PADDING_STRENGTH_INDICATOR, PasswordInput } from 'src/app/components/PasswordInput' import { Button, Flex, Text } from 'ui/src' import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring' -import { usePasswordForm } from 'wallet/src/utils/password' +import { PasswordErrors, usePasswordForm } from 'wallet/src/utils/password' -export function ChangePasswordForm({ onNext }: { onNext: (password: string) => void }): JSX.Element { +export function ChangePasswordForm({ + oldPassword, + onNext, +}: { + oldPassword: string | undefined + onNext: (password: string) => void +}): JSX.Element { const { t } = useTranslation() const { @@ -20,18 +26,45 @@ export function ChangePasswordForm({ onNext }: { onNext: (password: string) => v confirmPassword, onChangeConfirmPassword, setHideInput, - errorText, + errorText: baseErrorText, checkSubmit, } = usePasswordForm() + const [customError, setCustomError] = useState(undefined) + + // Check if new password is same as old password + const isSamePassword = useMemo( + () => Boolean(password && oldPassword && password === oldPassword), + [password, oldPassword], + ) + + // Update custom error when password matches old password + useEffect(() => { + setCustomError(isSamePassword ? PasswordErrors.SamePassword : undefined) + }, [isSamePassword]) + + // Override error text if custom error exists + const errorText = useMemo(() => { + if (customError === PasswordErrors.SamePassword) { + return t('common.input.password.error.same') + } + return baseErrorText + }, [customError, baseErrorText, t]) + const onSubmit = useCallback(async () => { + // Check for same password error + if (isSamePassword) { + setCustomError(PasswordErrors.SamePassword) + return + } + if (checkSubmit()) { // Just change the password and pass it to the parent await Keyring.changePassword(password) sendAnalyticsEvent(ExtensionEventName.PasswordChanged) onNext(password) } - }, [checkSubmit, password, onNext]) + }, [checkSubmit, password, onNext, isSamePassword]) return ( @@ -70,7 +103,7 @@ export function ChangePasswordForm({ onNext }: { onNext: (password: string) => v - diff --git a/apps/extension/src/app/features/settings/password/CreateNewPasswordModal.tsx b/apps/extension/src/app/features/settings/password/CreateNewPasswordModal.tsx index 65860afaa14..9411c1fa006 100644 --- a/apps/extension/src/app/features/settings/password/CreateNewPasswordModal.tsx +++ b/apps/extension/src/app/features/settings/password/CreateNewPasswordModal.tsx @@ -7,10 +7,12 @@ import { ModalName } from 'uniswap/src/features/telemetry/constants' export function CreateNewPasswordModal({ isOpen, + oldPassword, onNext, onClose, }: { isOpen: boolean + oldPassword: string | undefined onNext: (password: string) => void onClose: () => void }): JSX.Element { @@ -36,7 +38,7 @@ export function CreateNewPasswordModal({ {t('settings.setting.password.change.title')} - + ) diff --git a/apps/extension/src/app/features/settings/password/__snapshots__/ChangePasswordForm.test.tsx.snap b/apps/extension/src/app/features/settings/password/__snapshots__/ChangePasswordForm.test.tsx.snap new file mode 100644 index 00000000000..8dc96964d8e --- /dev/null +++ b/apps/extension/src/app/features/settings/password/__snapshots__/ChangePasswordForm.test.tsx.snap @@ -0,0 +1,287 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ChangePasswordForm renders without error 1`] = ` +{ + "asFragment": [Function], + "baseElement": +
+ +
+
+
+ + + +
+
+ + + +
+ + + +
+
+ +
+
+ +
+
+
+ +
+ , + "container":
+ +
+
+
+ + + +
+
+ + + +
+ + + +
+
+ +
+
+ +
+
+
+ +
, + "debug": [Function], + "findAllByAltText": [Function], + "findAllByDisplayValue": [Function], + "findAllByLabelText": [Function], + "findAllByPlaceholderText": [Function], + "findAllByRole": [Function], + "findAllByTestId": [Function], + "findAllByText": [Function], + "findAllByTitle": [Function], + "findByAltText": [Function], + "findByDisplayValue": [Function], + "findByLabelText": [Function], + "findByPlaceholderText": [Function], + "findByRole": [Function], + "findByTestId": [Function], + "findByText": [Function], + "findByTitle": [Function], + "getAllByAltText": [Function], + "getAllByDisplayValue": [Function], + "getAllByLabelText": [Function], + "getAllByPlaceholderText": [Function], + "getAllByRole": [Function], + "getAllByTestId": [Function], + "getAllByText": [Function], + "getAllByTitle": [Function], + "getByAltText": [Function], + "getByDisplayValue": [Function], + "getByLabelText": [Function], + "getByPlaceholderText": [Function], + "getByRole": [Function], + "getByTestId": [Function], + "getByText": [Function], + "getByTitle": [Function], + "queryAllByAltText": [Function], + "queryAllByDisplayValue": [Function], + "queryAllByLabelText": [Function], + "queryAllByPlaceholderText": [Function], + "queryAllByRole": [Function], + "queryAllByTestId": [Function], + "queryAllByText": [Function], + "queryAllByTitle": [Function], + "queryByAltText": [Function], + "queryByDisplayValue": [Function], + "queryByLabelText": [Function], + "queryByPlaceholderText": [Function], + "queryByRole": [Function], + "queryByTestId": [Function], + "queryByText": [Function], + "queryByTitle": [Function], + "rerender": [Function], + "store": { + "dispatch": [Function], + "getState": [Function], + "replaceReducer": [Function], + "subscribe": [Function], + Symbol(observable): [Function], + }, + "unmount": [Function], +} +`; diff --git a/apps/extension/src/app/features/settings/password/usePasswordResetFlow.test.ts b/apps/extension/src/app/features/settings/password/usePasswordResetFlow.test.ts index 9ccb4c71a73..3685be9bde1 100644 --- a/apps/extension/src/app/features/settings/password/usePasswordResetFlow.test.ts +++ b/apps/extension/src/app/features/settings/password/usePasswordResetFlow.test.ts @@ -67,6 +67,12 @@ describe('usePasswordResetFlow', () => { expect(result.current.flowState).toBe(PasswordResetFlowState.None) }) + it('should initialize with undefined oldPassword', () => { + const { result } = renderHook(() => usePasswordResetFlow()) + + expect(result.current.oldPassword).toBeUndefined() + }) + it('should transition to EnterCurrentPassword when starting password reset', () => { const { result } = renderHook(() => usePasswordResetFlow()) @@ -91,6 +97,22 @@ describe('usePasswordResetFlow', () => { expect(result.current.flowState).toBe(PasswordResetFlowState.EnterNewPassword) }) + it('should store oldPassword when valid password is provided', () => { + const { result } = renderHook(() => usePasswordResetFlow()) + const testPassword = 'myOldPassword123!' + + act(() => { + result.current.startPasswordReset() + }) + + act(() => { + result.current.onPasswordModalNext(testPassword) + }) + + expect(result.current.oldPassword).toBe(testPassword) + expect(result.current.flowState).toBe(PasswordResetFlowState.EnterNewPassword) + }) + it('should return to None state when no password is provided', () => { const { result } = renderHook(() => usePasswordResetFlow()) @@ -105,6 +127,32 @@ describe('usePasswordResetFlow', () => { expect(result.current.flowState).toBe(PasswordResetFlowState.None) }) + it('should clear oldPassword when no password is provided', () => { + const { result } = renderHook(() => usePasswordResetFlow()) + + act(() => { + result.current.startPasswordReset() + }) + + act(() => { + result.current.onPasswordModalNext('validPassword') + }) + + expect(result.current.oldPassword).toBe('validPassword') + + // Go back and provide no password + act(() => { + result.current.startPasswordReset() + }) + + act(() => { + result.current.onPasswordModalNext() + }) + + expect(result.current.oldPassword).toBeUndefined() + expect(result.current.flowState).toBe(PasswordResetFlowState.None) + }) + it('should transition to BiometricAuth when biometric is enabled', () => { mockUseHasBiometricUnlockCredential.mockReturnValue(true) const { result } = renderHook(() => usePasswordResetFlow()) @@ -160,6 +208,28 @@ describe('usePasswordResetFlow', () => { expect(result.current.flowState).toBe(PasswordResetFlowState.None) }) + it('should clear oldPassword when closeModal is called with matching state', () => { + const { result } = renderHook(() => usePasswordResetFlow()) + + act(() => { + result.current.startPasswordReset() + }) + + act(() => { + result.current.onPasswordModalNext('testPassword123') + }) + + expect(result.current.oldPassword).toBe('testPassword123') + expect(result.current.flowState).toBe(PasswordResetFlowState.EnterNewPassword) + + act(() => { + result.current.closeModal(PasswordResetFlowState.EnterNewPassword) + }) + + expect(result.current.flowState).toBe(PasswordResetFlowState.None) + expect(result.current.oldPassword).toBeUndefined() + }) + it('should not close modal when closeModal is called with non-matching state', () => { const { result } = renderHook(() => usePasswordResetFlow()) @@ -176,6 +246,29 @@ describe('usePasswordResetFlow', () => { expect(result.current.flowState).toBe(PasswordResetFlowState.EnterCurrentPassword) }) + it('should not clear oldPassword when closeModal is called with non-matching state', () => { + const { result } = renderHook(() => usePasswordResetFlow()) + + act(() => { + result.current.startPasswordReset() + }) + + act(() => { + result.current.onPasswordModalNext('testPassword456') + }) + + expect(result.current.oldPassword).toBe('testPassword456') + expect(result.current.flowState).toBe(PasswordResetFlowState.EnterNewPassword) + + act(() => { + result.current.closeModal(PasswordResetFlowState.BiometricAuth) + }) + + // Should not clear oldPassword or change state + expect(result.current.flowState).toBe(PasswordResetFlowState.EnterNewPassword) + expect(result.current.oldPassword).toBe('testPassword456') + }) + it('should transition to BiometricAuth state when biometric is enabled and trigger internal mutation', () => { mockUseHasBiometricUnlockCredential.mockReturnValue(true) const { result } = renderHook(() => usePasswordResetFlow()) diff --git a/apps/extension/src/app/features/settings/password/usePasswordResetFlow.ts b/apps/extension/src/app/features/settings/password/usePasswordResetFlow.ts index 48e02692ef5..9c2b61b4eda 100644 --- a/apps/extension/src/app/features/settings/password/usePasswordResetFlow.ts +++ b/apps/extension/src/app/features/settings/password/usePasswordResetFlow.ts @@ -54,6 +54,7 @@ export enum PasswordResetFlowState { interface PasswordResetFlowResult { // State flowState: PasswordResetFlowState + oldPassword: string | undefined // Actions startPasswordReset: () => void @@ -68,6 +69,7 @@ interface PasswordResetFlowResult { export function usePasswordResetFlow(): PasswordResetFlowResult { const dispatch = useDispatch() const [flowState, setFlowState] = useState(PasswordResetFlowState.None) + const [oldPassword, setOldPassword] = useState(undefined) const hasBiometricUnlockCredential = useHasBiometricUnlockCredential() @@ -95,15 +97,18 @@ export function usePasswordResetFlow(): PasswordResetFlowResult { // This check ensures the close action is from user interaction, not from modal state changes. if (flowState === expectedState) { setFlowState(PasswordResetFlowState.None) + setOldPassword(undefined) } }) const onPasswordModalNext = useEvent((password?: string): void => { if (!password) { setFlowState(PasswordResetFlowState.None) + setOldPassword(undefined) return } + setOldPassword(password) setFlowState(PasswordResetFlowState.EnterNewPassword) }) @@ -127,6 +132,7 @@ export function usePasswordResetFlow(): PasswordResetFlowResult { return { // State flowState, + oldPassword, // Actions startPasswordReset, diff --git a/apps/extension/src/app/navigation/navigation.tsx b/apps/extension/src/app/navigation/navigation.tsx index 7c0848212a5..a9934d18846 100644 --- a/apps/extension/src/app/navigation/navigation.tsx +++ b/apps/extension/src/app/navigation/navigation.tsx @@ -202,7 +202,7 @@ const AnimatedPane = styled(Flex, { const isVertical = (dir: Direction): boolean => dir === 'up' || dir === 'down' function useConstant(c: A): A { - const out = useRef() + const out = useRef(undefined) if (!out.current) { out.current = c } diff --git a/apps/extension/src/app/saga.ts b/apps/extension/src/app/saga.ts index a5e6ddbf951..e4c17a340bd 100644 --- a/apps/extension/src/app/saga.ts +++ b/apps/extension/src/app/saga.ts @@ -28,13 +28,6 @@ import { prepareAndSignSwapSaga, prepareAndSignSwapSagaName, } from 'wallet/src/features/transactions/swap/configuredSagas' -import { swapActions, swapReducer, swapSaga, swapSagaName } from 'wallet/src/features/transactions/swap/swapSaga' -import { - tokenWrapActions, - tokenWrapReducer, - tokenWrapSaga, - tokenWrapSagaName, -} from 'wallet/src/features/transactions/swap/wrapSaga' import { watchTransactionEvents } from 'wallet/src/features/transactions/watcher/transactionFinalizationSaga' import { transactionWatcher } from 'wallet/src/features/transactions/watcher/transactionWatcherSaga' import { @@ -83,18 +76,6 @@ export const monitoredSagas: Record = { reducer: executeSwapReducer, actions: executeSwapActions, }, - [swapSagaName]: { - name: swapSagaName, - wrappedSaga: swapSaga, - reducer: swapReducer, - actions: swapActions, - }, - [tokenWrapSagaName]: { - name: tokenWrapSagaName, - wrappedSaga: tokenWrapSaga, - reducer: tokenWrapReducer, - actions: tokenWrapActions, - }, [removeDelegationSagaName]: { name: removeDelegationSagaName, wrappedSaga: removeDelegationSaga, diff --git a/apps/extension/src/app/utils/analytics.ts b/apps/extension/src/app/utils/analytics.ts index b24911e58b1..41fb45f6b41 100644 --- a/apps/extension/src/app/utils/analytics.ts +++ b/apps/extension/src/app/utils/analytics.ts @@ -5,11 +5,18 @@ import 'symbol-observable' // Needed by `reduxed-chrome-storage` as polyfill, or import { EXTENSION_ORIGIN_APPLICATION } from 'src/app/version' import { uniswapUrls } from 'uniswap/src/constants/urls' import { getUniqueId } from 'utilities/src/device/uniqueId' +import { isTestEnv } from 'utilities/src/environment/env' +import { logger } from 'utilities/src/logger/logger' import { ApplicationTransport } from 'utilities/src/telemetry/analytics/ApplicationTransport' // biome-ignore lint/style/noRestrictedImports: Direct utilities import required for analytics initialization import { analytics, getAnalyticsAtomDirect } from 'utilities/src/telemetry/analytics/analytics' export async function initExtensionAnalytics(): Promise { + if (isTestEnv()) { + logger.debug('analytics.ts', 'initExtensionAnalytics', 'Skipping Amplitude initialization in test environment') + return + } + const analyticsAllowed = await getAnalyticsAtomDirect(true) await analytics.init({ transportProvider: new ApplicationTransport({ diff --git a/apps/extension/src/background/backgroundDappRequests.ts b/apps/extension/src/background/backgroundDappRequests.ts index 968024943b1..e256ed0fb19 100644 --- a/apps/extension/src/background/backgroundDappRequests.ts +++ b/apps/extension/src/background/backgroundDappRequests.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-lines */ import { rpcErrors, serializeError } from '@metamask/rpc-errors' import { removeDappConnection } from 'src/app/features/dapp/actions' import { changeChain } from 'src/app/features/dapp/changeChain' @@ -32,7 +33,7 @@ import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { WindowEthereumRequestProperties } from 'uniswap/src/features/telemetry/types' import { extractBaseUrl } from 'utilities/src/format/urls' import { logger } from 'utilities/src/logger/logger' -import { getCapabilitiesCore } from 'wallet/src/features/batchedTransactions/utils' +import { getCapabilitiesResponse } from 'wallet/src/features/batchedTransactions/utils' import { walletContextValue } from 'wallet/src/features/wallet/context' import { selectHasSmartWalletConsent } from 'wallet/src/features/wallet/selectors' @@ -320,7 +321,7 @@ async function handleGetCapabilities({ }) const chainIds = request.chainIds?.map(hexadecimalStringToInt) ?? enabledChains.map((chain) => chain.valueOf()) - const response = await getCapabilitiesCore({ + const response = await getCapabilitiesResponse({ request, chainIds, hasSmartWalletConsent, diff --git a/apps/extension/src/background/utils/persistedStateUtils.ts b/apps/extension/src/background/utils/persistedStateUtils.ts index 50f2b49cdc3..2b646c64565 100644 --- a/apps/extension/src/background/utils/persistedStateUtils.ts +++ b/apps/extension/src/background/utils/persistedStateUtils.ts @@ -2,6 +2,7 @@ import { isOnboardedSelector } from 'src/app/utils/isOnboardedSelector' import { STATE_STORAGE_KEY } from 'src/store/constants' import { ExtensionState } from 'src/store/extensionReducer' import { EXTENSION_STATE_VERSION } from 'src/store/migrations' +import { deviceAccessTimeoutToMinutes } from 'uniswap/src/features/settings/constants' import { logger } from 'utilities/src/logger/logger' export async function readReduxStateFromStorage(storageChanges?: { @@ -30,6 +31,11 @@ export async function readIsOnboardedFromStorage(): Promise { return state ? isOnboardedSelector(state) : false } +export async function readDeviceAccessTimeoutMinutesFromStorage(): Promise { + const state = await readReduxStateFromStorage() + return state ? deviceAccessTimeoutToMinutes(state.userSettings.deviceAccessTimeout) : undefined +} + /** * Checks if Redux migrations are pending by comparing persisted version with current version * @returns true if migrations are pending and sidebar should handle the request diff --git a/apps/extension/src/entrypoints/background.ts b/apps/extension/src/entrypoints/background.ts index f75bfd172b9..773b1b9114a 100644 --- a/apps/extension/src/entrypoints/background.ts +++ b/apps/extension/src/entrypoints/background.ts @@ -1,5 +1,6 @@ import 'symbol-observable' // Needed by `reduxed-chrome-storage` as polyfill, order matters +import { AUTO_LOCK_ALARM_NAME } from 'src/app/components/AutoLockProvider' import { initStatSigForBrowserScripts } from 'src/app/core/initStatSigForBrowserScripts' import { focusOrCreateOnboardingTab } from 'src/app/navigation/focusOrCreateOnboardingTab' import { initExtensionAnalytics } from 'src/app/utils/analytics' @@ -14,9 +15,15 @@ import { ContentScriptUtilityMessageType, } from 'src/background/messagePassing/types/requests' import { setSidePanelBehavior, setSidePanelOptions } from 'src/background/utils/chromeSidePanelUtils' -import { readIsOnboardedFromStorage } from 'src/background/utils/persistedStateUtils' +import { + readDeviceAccessTimeoutMinutesFromStorage, + readIsOnboardedFromStorage, +} from 'src/background/utils/persistedStateUtils' import { uniswapUrls } from 'uniswap/src/constants/urls' +import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants' +import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { logger } from 'utilities/src/logger/logger' +import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring' import { defineBackground } from 'wxt/utils/define-background' async function enableSidebar(): Promise { @@ -69,6 +76,53 @@ function makeBackground(): void { await checkAndHandleOnboarding() }) + // Auto-lock alarm listener + chrome.alarms.onAlarm.addListener((alarm) => { + if (alarm.name === AUTO_LOCK_ALARM_NAME) { + Keyring.lock() + .then(() => { + sendAnalyticsEvent(ExtensionEventName.ChangeLockedState, { + locked: true, + location: 'background', + }) + }) + .catch((error) => { + logger.error(error, { + tags: { + file: 'background.ts', + function: 'alarms.onAlarm', + }, + }) + }) + } + }) + + // Listen for sidebar port disconnects to schedule auto-lock alarm + chrome.runtime.onConnect.addListener((port) => { + if (port.name === AUTO_LOCK_ALARM_NAME) { + port.onDisconnect.addListener(async () => { + try { + // Get timeout setting from Redux state + const delayInMinutes = await readDeviceAccessTimeoutMinutesFromStorage() + if (delayInMinutes === undefined) { + return + } + + // Schedule alarm + chrome.alarms.create(AUTO_LOCK_ALARM_NAME, { delayInMinutes }) + logger.debug('background', 'port.onDisconnect', `Scheduled auto-lock alarm for ${delayInMinutes} minutes`) + } catch (error) { + logger.error(error, { + tags: { + file: 'background.ts', + function: 'port.onDisconnect', + }, + }) + } + }) + } + }) + // on arc browser, show unsupported browser page (lives on onboarding flow) // this is because arc doesn't support the sidebar contentScriptUtilityMessageChannel.addMessageListener( diff --git a/apps/extension/src/manifest.json b/apps/extension/src/manifest.json index 4b599a3f4bd..e7bfeae6572 100644 --- a/apps/extension/src/manifest.json +++ b/apps/extension/src/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "Uniswap Extension", "description": "The Uniswap Extension is a self-custody crypto wallet that's built for swapping.", - "version": "1.61.0", + "version": "1.64.0", "minimum_chrome_version": "116", "icons": { "16": "assets/icon16.png", diff --git a/apps/extension/src/store/migrations.test.ts b/apps/extension/src/store/migrations.test.ts index f78dd2d2f16..fe092e7fcae 100644 --- a/apps/extension/src/store/migrations.test.ts +++ b/apps/extension/src/store/migrations.test.ts @@ -36,7 +36,10 @@ import { v24Schema, v25Schema, v26Schema, + v27Schema, + v29Schema, } from 'src/store/schema' +import { USDC } from 'uniswap/src/constants/tokens' import { initialUniswapBehaviorHistoryState } from 'uniswap/src/features/behaviorHistory/slice' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { initialFavoritesState } from 'uniswap/src/features/favorites/slice' @@ -44,11 +47,16 @@ import { FiatCurrency } from 'uniswap/src/features/fiatCurrency/constants' import { initialNotificationsState } from 'uniswap/src/features/notifications/slice/slice' import { initialSearchHistoryState } from 'uniswap/src/features/search/searchHistorySlice' import { initialUserSettingsState } from 'uniswap/src/features/settings/slice' -import { initialTokensState } from 'uniswap/src/features/tokens/slice/slice' +import { initialTokensState } from 'uniswap/src/features/tokens/warnings/slice/slice' import { initialTransactionsState } from 'uniswap/src/features/transactions/slice' import { TransactionStatus, TransactionType } from 'uniswap/src/features/transactions/types/transactionDetails' import { initialVisibilityState } from 'uniswap/src/features/visibility/slice' -import { testMigrateSearchHistory, testRemoveTHBFromCurrency } from 'uniswap/src/state/uniswapMigrationTests' +import { + testAddActivityVisibility, + testMigrateDismissedTokenWarnings, + testMigrateSearchHistory, + testRemoveTHBFromCurrency, +} from 'uniswap/src/state/uniswapMigrationTests' import { getAllKeysOfNestedObject } from 'utilities/src/primitives/objects' import { initialAppearanceSettingsState } from 'wallet/src/features/appearance/slice' import { initialBatchedTransactionsState } from 'wallet/src/features/batchedTransactions/slice' @@ -347,4 +355,24 @@ describe('Redux state migrations', () => { it('migrates from v26 to v27', () => { testMigrateSearchHistory(migrations[27], v26Schema) }) + + it('migrates from v27 to v29', () => { + testAddActivityVisibility(migrations[29], v27Schema) + }) + + it('migrates from v29 to v30', () => { + testMigrateDismissedTokenWarnings(migrations[30], { + ...v29Schema, + tokens: { + dismissedTokenWarnings: { + [UniverseChainId.Mainnet]: { + [USDC.address]: { + chainId: UniverseChainId.Mainnet, + address: USDC.address, + }, + }, + }, + }, + }) + }) }) diff --git a/apps/extension/src/store/migrations.ts b/apps/extension/src/store/migrations.ts index e9534188698..54f1e83a420 100644 --- a/apps/extension/src/store/migrations.ts +++ b/apps/extension/src/store/migrations.ts @@ -7,7 +7,9 @@ import { removeDappInfoToChromeLocalStorage, } from 'src/store/extensionMigrations' import { + addActivityVisibility, addDismissedBridgedAndCompatibleWarnings, + migrateDismissedTokenWarnings, migrateSearchHistory, removeThaiBahtFromFiatCurrency, unchecksumDismissedTokenWarningKeys, @@ -67,6 +69,8 @@ export const migrations = { 26: migrateLiquidityTransactionInfo, 27: migrateSearchHistory, 28: addDismissedBridgedAndCompatibleWarnings, + 29: addActivityVisibility, + 30: migrateDismissedTokenWarnings, } -export const EXTENSION_STATE_VERSION = 28 +export const EXTENSION_STATE_VERSION = 30 diff --git a/apps/extension/src/store/schema.ts b/apps/extension/src/store/schema.ts index 0fd7dc87697..f7c21de465c 100644 --- a/apps/extension/src/store/schema.ts +++ b/apps/extension/src/store/schema.ts @@ -276,6 +276,10 @@ export const v25Schema = { ...v24Schema } export const v26Schema = { ...v25Schema } -const v27Schema = { ...v26Schema } +export const v27Schema = { ...v26Schema } -export const getSchema = (): typeof v27Schema => v27Schema +export const v29Schema = { ...v27Schema, visibility: { ...v27Schema.visibility, activity: {} } } + +const v30Schema = { ...v29Schema } + +export const getSchema = (): typeof v30Schema => v30Schema diff --git a/apps/extension/src/test/babel.config.js b/apps/extension/src/test/babel.config.js index 9118275b087..7913db21158 100644 --- a/apps/extension/src/test/babel.config.js +++ b/apps/extension/src/test/babel.config.js @@ -16,10 +16,11 @@ module.exports = function (api) { ], // https://github.com/software-mansion/react-native-reanimated/issues/3364#issuecomment-1268591867 '@babel/plugin-proposal-export-namespace-from', + 'react-native-reanimated/plugin', ].filter(Boolean) return { - presets: ['module:@react-native/babel-preset'], + presets: ['babel-preset-expo'], plugins, } } diff --git a/apps/extension/src/test/fixtures/redux.ts b/apps/extension/src/test/fixtures/redux.ts index fbdbe85ec9b..773a07165bf 100644 --- a/apps/extension/src/test/fixtures/redux.ts +++ b/apps/extension/src/test/fixtures/redux.ts @@ -5,8 +5,13 @@ import { preloadedWalletPackageState } from 'wallet/src/test/fixtures' type PreloadedExtensionStateOptions = Record -export const preloadedExtensionState = createFixture, PreloadedExtensionStateOptions>( - {}, -)(() => ({ +type PreloadedExtensionStateFactory = ( + overrides?: Partial & PreloadedExtensionStateOptions>, +) => PreloadedState + +export const preloadedExtensionState: PreloadedExtensionStateFactory = createFixture< + PreloadedState, + PreloadedExtensionStateOptions +>({})(() => ({ ...preloadedWalletPackageState(), })) diff --git a/apps/extension/tsconfig.json b/apps/extension/tsconfig.json index 831ce04a0d1..fe4165ad91e 100644 --- a/apps/extension/tsconfig.json +++ b/apps/extension/tsconfig.json @@ -17,6 +17,12 @@ { "path": "../../packages/ui" }, + { + "path": "../../packages/sessions" + }, + { + "path": "../../packages/gating" + }, { "path": "../../packages/api" }, diff --git a/apps/extension/webpack.config.js b/apps/extension/webpack.config.js index 2893c62943f..06ffc015ebf 100644 --- a/apps/extension/webpack.config.js +++ b/apps/extension/webpack.config.js @@ -29,6 +29,7 @@ const compileNodeModules = [ 'expo-linear-gradient', 'react-native-image-picker', 'expo-modules-core', + 'react-native-reanimated', ] // This is needed for webpack to compile JavaScript. @@ -48,8 +49,8 @@ const babelLoaderConfiguration = { loader: 'babel-loader', options: { cacheDirectory: true, - // The 'metro-react-native-babel-preset' preset is recommended to match React Native's packager - presets: ['module:@react-native/babel-preset'], + // The 'babel-preset-expo' preset is recommended to match React Native's packager + presets: ['babel-preset-expo'], // Re-write paths to import only the modules needed by the app plugins: ['react-native-web'], }, diff --git a/apps/extension/wxt.config.ts b/apps/extension/wxt.config.ts index 37e4fc458e7..6fa77e81623 100644 --- a/apps/extension/wxt.config.ts +++ b/apps/extension/wxt.config.ts @@ -16,7 +16,7 @@ const icons = { const BASE_NAME = 'Uniswap Extension' const BASE_DESCRIPTION = "The Uniswap Extension is a self-custody crypto wallet that's built for swapping." -const BASE_VERSION = '1.61.0' +const BASE_VERSION = '1.64.0' const BUILD_NUM = parseInt(process.env.BUILD_NUM || '0') const EXTENSION_VERSION = `${BASE_VERSION}.${BUILD_NUM}` @@ -187,10 +187,14 @@ export default defineConfig({ plugins: [ { - name: 'transform-expo-blur-jsx', + name: 'transform-react-native-jsx', async transform(code, id) { - // Only transform expo-blur .js files - if (!id.includes('node_modules/expo-blur') || !id.endsWith('.js')) { + // Transform JSX in react-native libraries that ship JSX in .js files + const needsJsxTransform = ['node_modules/expo-blur', 'node_modules/react-native-reanimated'].some((path) => + id.includes(path), + ) + + if (!needsJsxTransform || !id.endsWith('.js')) { return null } diff --git a/apps/mobile/.depcheckrc b/apps/mobile/.depcheckrc index 6190990de06..3ef594a9703 100644 --- a/apps/mobile/.depcheckrc +++ b/apps/mobile/.depcheckrc @@ -12,7 +12,7 @@ ignores: [ "babel-plugin-transform-remove-console", "cross-fetch", "@datadog/datadog-ci", - "@rnef/cli", + "dotenv", "expo-localization", "expo-linking", "expo-modules-core", @@ -43,4 +43,7 @@ ignores: [ "metro-config", ## used in sessions/api packages "expo-secure-store", + ## used for expo remote build caching + "eas-build-cache-provider", + "expo-dev-client", ] diff --git a/apps/mobile/.eslintrc.js b/apps/mobile/.eslintrc.js index 8a2ee3a2cee..26a62d9f007 100644 --- a/apps/mobile/.eslintrc.js +++ b/apps/mobile/.eslintrc.js @@ -58,13 +58,6 @@ module.exports = { 'import/no-unused-modules': 'off', }, }, - { - // Allow test files to exceed max-lines limit - files: ['**/*.test.ts', '**/*.test.tsx', '**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[tj]s?(x)'], - rules: { - 'max-lines': 'off', - }, - }, ], rules: { 'rulesdir/i18n': 'error', diff --git a/apps/mobile/.fingerprintignore b/apps/mobile/.fingerprintignore new file mode 100644 index 00000000000..2bcbe3b6c40 --- /dev/null +++ b/apps/mobile/.fingerprintignore @@ -0,0 +1,49 @@ +# Generated files that shouldn't trigger rebuilds +ios/WidgetsCore/MobileSchema/**/*.swift +ios/WidgetsCore/Env.swift +ios/OneSignalNotificationServiceExtension/Env.swift +.maestro/scripts/dist/**/* +.maestro/scripts/performance/dist/**/* + +# Cache/temporary files +.expo/**/* +coverage/**/* +.tamagui/**/* +storybook-static/**/* +dist/**/* +build/**/* + +# Environment files +.env* +!.env.example + +# All node_modules - native deps are tracked via lock files +node_modules/**/* +../../node_modules/**/* + +# Build configuration that doesn't affect native +.gitignore + +# Autolinking outputs (redundant with lock files) +# These are derived from node_modules and package.json +expoAutolinkingConfig:android +expoAutolinkingConfig:ios +rncoreAutolinkingConfig:android +rncoreAutolinkingConfig:ios + +# Android build artifacts (if not already in native .gitignore) +android/app/build/**/* +android/.gradle/**/* +android/build/**/* +android/.cxx/**/* + +# iOS build artifacts (if not already in native .gitignore) +ios/build/**/* +ios/Pods/**/* +!ios/Podfile +!ios/Podfile.lock +ios/*.xcworkspace/**/* +!ios/*.xcworkspace/contents.xcworkspacedata + +# ensure patches are tracked +!../../patches/**/* diff --git a/apps/mobile/.gitignore b/apps/mobile/.gitignore index f6487b4f10a..6d1ce5cd0e8 100644 --- a/apps/mobile/.gitignore +++ b/apps/mobile/.gitignore @@ -36,6 +36,7 @@ local.properties *.jks keystore.properties *.aab +.kotlin/ # node.js # @@ -120,8 +121,16 @@ ios/WidgetsCore/Env.swift ios/OneSignalNotificationServiceExtension/Env.swift # Expo -.expo/ +.expo +dist/ +web-build/ # Maestro E2E Scripts (compiled/generated) .maestro/scripts/dist/ .maestro/scripts/performance/dist/ + +# rnef (deprecated) +.rnef/ + +coverage/ + diff --git a/apps/mobile/.maestro/flows/explore/filters-and-sorts.yaml b/apps/mobile/.maestro/flows/explore/filters-and-sorts.yaml index 89740a599f7..76c7fa635fe 100644 --- a/apps/mobile/.maestro/flows/explore/filters-and-sorts.yaml +++ b/apps/mobile/.maestro/flows/explore/filters-and-sorts.yaml @@ -1,7 +1,5 @@ appId: com.uniswap.mobile.dev jsEngine: graaljs -tags: - - language-agnostic env: E2E_RECOVERY_PHRASE: ${E2E_RECOVERY_PHRASE} DATADOG_API_KEY: ${DATADOG_API_KEY} @@ -72,11 +70,11 @@ env: # Wait for sort options to appear - extendedWaitUntil: visible: - id: "MARKET_CAP" + text: ".*Market.*" timeout: 3000 - tapOn: - id: "MARKET_CAP" + text: ".*Market.*" - runScript: file: ../../scripts/performance/dist/actions/track-action.js @@ -106,11 +104,11 @@ env: - waitForAnimationToEnd - extendedWaitUntil: visible: - id: "TOTAL_VALUE_LOCKED" + text: ".*TVL.*" timeout: 3000 - tapOn: - id: "TOTAL_VALUE_LOCKED" + text: ".*TVL.*" - runScript: file: ../../scripts/performance/dist/actions/track-action.js @@ -142,11 +140,11 @@ env: # Wait for sort options to appear - extendedWaitUntil: visible: - id: "PRICE_PERCENT_CHANGE_1_DAY_DESC" + text: ".*Price.*increase.*" timeout: 3000 - tapOn: - id: "PRICE_PERCENT_CHANGE_1_DAY_DESC" + text: ".*Price.*increase.*" - runScript: file: ../../scripts/performance/dist/actions/track-action.js @@ -178,11 +176,11 @@ env: # Wait for sort options to appear - extendedWaitUntil: visible: - id: "PRICE_PERCENT_CHANGE_1_DAY_ASC" + text: ".*Price.*decrease.*" timeout: 3000 - tapOn: - id: "PRICE_PERCENT_CHANGE_1_DAY_ASC" + text: ".*Price.*decrease.*" - runScript: file: ../../scripts/performance/dist/actions/track-action.js @@ -214,11 +212,11 @@ env: # Wait for sort options to appear - extendedWaitUntil: visible: - id: "VOLUME" + text: ".*Volume.*" timeout: 3000 - tapOn: - id: "VOLUME" + text: ".*Volume.*" - runScript: file: ../../scripts/performance/dist/actions/track-action.js diff --git a/apps/mobile/android/app/build.gradle b/apps/mobile/android/app/build.gradle index cf470808dde..8f7e2bd848a 100644 --- a/apps/mobile/android/app/build.gradle +++ b/apps/mobile/android/app/build.gradle @@ -1,40 +1,35 @@ -import com.android.build.OutputFile - -plugins { - id 'com.android.application' - id 'com.facebook.react' - id 'com.google.gms.google-services' - id 'maven-publish' - id 'kotlin-android' - id 'org.jetbrains.kotlin.plugin.compose' -} +apply plugin: "com.android.application" +apply plugin: "org.jetbrains.kotlin.android" +apply plugin: "com.google.gms.google-services" +apply plugin: "maven-publish" +apply plugin: "kotlin-android" +apply plugin: "org.jetbrains.kotlin.plugin.compose" +apply plugin: "com.facebook.react" -def nodeModulesPath = "../../../../node_modules" -def rnRoot = "../.." +def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath() -def keystorePropertiesFile = rootProject.file("keystore.properties"); -def keystoreProperties = new Properties() -if (keystorePropertiesFile.exists()) { - keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) -} +def nodeModulesPath = "../../../../node_modules" react { - root = file("$rnRoot/") + // From expo docs: https://docs.expo.dev/brownfield/get-started + entryFile = file(["node", "-e", "require('expo/scripts/resolveAppEntry')", rootDir.getAbsoluteFile().getParentFile().getAbsolutePath(), "android", "absolute"].execute(null, rootDir).text.trim()) + reactNativeDir = file("$nodeModulesPath/react-native") - codegenDir = file("$nodeModulesPath/react-native-codegen") - cliFile = file("$nodeModulesPath/@rnef/cli/dist/src/bin.js") + + hermesCommand = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/sdks/hermesc/%OS-BIN%/hermesc" + + codegenDir = new File(["node", "--print", "require.resolve('@react-native/codegen/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile() + + enableBundleCompression = (findProperty('android.enableBundleCompression') ?: false).toBoolean() + + cliFile = new File(["node", "--print", "require.resolve('@expo/cli')"].execute(null, rootDir).text.trim()) + bundleCommand = "export:embed" + debuggableVariants = ["devDebug", "betaDebug", "prodDebug"] - hermesCommand = "../../node_modules/react-native/sdks/hermesc/%OS-BIN%/hermesc" // This is relative to the project root. + /* Autolinking */ autolinkLibrariesWithApp() } -/** - * Set this to true to create four separate APKs instead of one, - * one for each native architecture. This is useful if you don't - * use App Bundles (https://developer.android.com/guide/app-bundle/) - * and want to have separate APKs to upload to the Play Store. - */ -def enableSeparateBuildPerCPUArchitecture = false /** * Set this to true to Run Proguard on Release builds to minify the Java bytecode. @@ -45,14 +40,14 @@ def enableProguardInReleaseBuilds = false * The preferred build flavor of JavaScriptCore (JSC) * * For example, to use the international variant, you can use: - * `def jscFlavor = 'org.webkit:android-jsc-intl:+'` + * `def jscFlavor = io.github.react-native-community:jsc-android-intl:2026004.+` * * The international variant includes ICU i18n library and necessary data * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that * give correct results when using with locales other than en-US. Note that * this variant is about 6MiB larger per architecture than default. */ -def jscFlavor = 'org.webkit:android-jsc-intl:+' +def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+' /** * Private function to get the list of Native Architectures you want to build. @@ -72,9 +67,9 @@ if (isCI && datadogPropertiesAvailable) { apply from: "../../../../node_modules/@datadog/mobile-react-native/datadog-sourcemaps.gradle" } -def devVersionName = "1.61" -def betaVersionName = "1.61" -def prodVersionName = "1.61" +def devVersionName = "1.64.1" +def betaVersionName = "1.64.1" +def prodVersionName = "1.64.1" android { ndkVersion rootProject.ext.ndkVersion @@ -91,7 +86,7 @@ android { splits { abi { reset() - enable enableSeparateBuildPerCPUArchitecture + enable false universalApk false // If true, also generate a universal APK include (*reactNativeArchitectures()) } @@ -107,10 +102,18 @@ android { keyPassword 'android' } release { - storeFile file(System.getenv("ANDROID_KEYSTORE_FILE") ?: 'keystore.jks') - storePassword System.getenv("ANDROID_STORE_PASSWORD") ?: keystoreProperties.getProperty("STORE_PASSWORD") - keyAlias System.getenv("ANDROID_KEYSTORE_ALIAS") ?: keystoreProperties.getProperty("KEYSTORE_ALIAS") - keyPassword System.getenv("ANDROID_KEY_PASSWORD") ?: keystoreProperties.getProperty("KEY_PASSWORD") + def useDebugKeystore = System.getenv("ANDROID_USE_DEBUG_KEYSTORE") == "true" + if (useDebugKeystore) { + storeFile file('debug.keystore') + storePassword 'android' + keyAlias 'androiddebugkey' + keyPassword 'android' + } else { + storeFile file(System.getenv("ANDROID_KEYSTORE_FILE") ?: 'keystore.jks') + storePassword System.getenv("ANDROID_STORE_PASSWORD") ?: "" + keyAlias System.getenv("ANDROID_KEYSTORE_ALIAS") ?: "" + keyPassword System.getenv("ANDROID_KEY_PASSWORD") ?: "" + } } } @@ -119,12 +122,12 @@ android { productFlavors { dev { isDefault(true) - applicationIdSuffix ".dev" + applicationId "com.uniswap.mobile.dev" versionName devVersionName dimension "variant" } beta { - applicationIdSuffix ".beta" + applicationId "com.uniswap.mobile.beta" versionName betaVersionName dimension "variant" } @@ -146,18 +149,33 @@ android { } // applicationVariants are e.g. debug, release - applicationVariants.all { variant -> + applicationVariants.configureEach { variant -> + // Prevent using debug keystore for production builds + if (variant.flavorName == "prod" && variant.buildType.name == "release") { + def useDebugKeystore = System.getenv("ANDROID_USE_DEBUG_KEYSTORE") == "true" + if (useDebugKeystore) { + def blockTask = tasks.register("blockDebugKeystoreFor${variant.name.capitalize()}") { + doLast { + throw new GradleException( + "ANDROID_USE_DEBUG_KEYSTORE cannot be used for production builds.\n" + + "This prevents accidentally publishing an improperly signed APK." + ) + } + } + variant.assembleProvider.configure { dependsOn blockTask } + } + } + variant.outputs.each { output -> // For each separate APK per architecture, set a unique version code as described here: // https://developer.android.com/studio/build/configure-apk-splits.html // Example: versionCode 1 will generate 1001 for armeabi-v7a, 1002 for x86, etc. def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4] - def abi = output.getFilter(OutputFile.ABI) + def abi = output.getFilter(com.android.build.VariantOutput.FilterType.ABI) if (abi != null) { // null for the universal-debug, universal-release variants output.versionCodeOverride = defaultConfig.versionCode * 1000 + versionCodes.get(abi) } - } } @@ -193,6 +211,7 @@ android { dependencies { // The version of react-native is set by the React Native Gradle Plugin implementation "com.facebook.react:react-android" + implementation("com.facebook.react:hermes-android") implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" @@ -247,10 +266,4 @@ dependencies { implementation 'androidx.compose.ui:ui-tooling-preview-android:1.8.1' implementation project(':react-native-video') - - if (hermesEnabled.toBoolean()) { - implementation("com.facebook.react:hermes-android") - } else { - implementation jscFlavor - } } diff --git a/apps/mobile/android/app/src/main/AndroidManifest.xml b/apps/mobile/android/app/src/main/AndroidManifest.xml index 66bdd248726..aed03ffb4b9 100644 --- a/apps/mobile/android/app/src/main/AndroidManifest.xml +++ b/apps/mobile/android/app/src/main/AndroidManifest.xml @@ -7,7 +7,7 @@ - + + + - + - - + + - - + + @@ -77,39 +81,39 @@ + android:scheme="https" + android:host="app.uniswap.org" + android:pathPrefix="/nfts/asset/" /> + android:scheme="https" + android:host="app.uniswap.org" + android:pathPrefix="/nfts/collection/" /> + android:scheme="https" + android:host="app.uniswap.org" + android:pathPrefix="/tokens" /> + android:scheme="https" + android:host="app.uniswap.org" + android:pathPrefix="/address/" /> + android:scheme="https" + android:host="app.uniswap.org" + android:pathPrefix="/explore/" /> + android:scheme="https" + android:host="app.uniswap.org" + android:pathPrefix="/swap" /> + android:scheme="https" + android:host="app.uniswap.org" + android:pathPrefix="/buy" /> @@ -118,14 +122,14 @@ + android:scheme="https" + android:host="uniswap.org" + android:pathPrefix="/app" /> + android:scheme="https" + android:host="uniswap.org" + android:pathPrefix="/app/wc" /> diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/MainActivity.kt b/apps/mobile/android/app/src/main/java/com/uniswap/MainActivity.kt index 370ffb03d1f..ee40024b0a3 100644 --- a/apps/mobile/android/app/src/main/java/com/uniswap/MainActivity.kt +++ b/apps/mobile/android/app/src/main/java/com/uniswap/MainActivity.kt @@ -44,10 +44,6 @@ class MainActivity : ReactActivity() { * (aka React 18) with two boolean flags. */ override fun createReactActivityDelegate(): ReactActivityDelegate? { - return ReactActivityDelegateWrapper( - this, - BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, - DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled) - ) + return ReactActivityDelegateWrapper(this, BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)) } } diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/MainApplication.kt b/apps/mobile/android/app/src/main/java/com/uniswap/MainApplication.kt index b914ee2c34a..21c5c32714a 100644 --- a/apps/mobile/android/app/src/main/java/com/uniswap/MainApplication.kt +++ b/apps/mobile/android/app/src/main/java/com/uniswap/MainApplication.kt @@ -19,7 +19,7 @@ import expo.modules.ReactNativeHostWrapper class MainApplication : Application(), ReactApplication { override val reactNativeHost: ReactNativeHost = - ReactNativeHostWrapper(this, object : DefaultReactNativeHost(this) { + ReactNativeHostWrapper(this, object : DefaultReactNativeHost(this) { override fun getPackages(): List = PackageList(this).packages.apply { // Packages that cannot be autolinked yet can be added manually here, for example: @@ -29,9 +29,7 @@ class MainApplication : Application(), ReactApplication { add(ScantasticEncryptionModule()) add(RedirectToSourceAppPackage()) } - override fun getJSMainModuleName(): String { - return "index" - } + override fun getJSMainModuleName(): String = ".expo/.virtual-metro-entry" override fun getUseDeveloperSupport(): Boolean { return BuildConfig.DEBUG @@ -39,22 +37,23 @@ class MainApplication : Application(), ReactApplication { override val isNewArchEnabled: Boolean get() = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED - - override val isHermesEnabled: Boolean - get() = BuildConfig.IS_HERMES_ENABLED }) override val reactHost: ReactHost - get() = getDefaultReactHost(applicationContext, reactNativeHost) + get() = ReactNativeHostWrapper.createReactHost(applicationContext, reactNativeHost) override fun onCreate() { ReactNativePerformance.onAppStarted() super.onCreate() + + // Initialize SoLoader before any code that might load native libraries SoLoader.init(this, OpenSourceMergedSoMapping) if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { // If you opted-in for the New Architecture, we load the native entry point for this app. load() } + + // Initialize Expo modules after SoLoader ApplicationLifecycleDispatcher.onApplicationCreate(this) } diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/UniswapPackage.kt b/apps/mobile/android/app/src/main/java/com/uniswap/UniswapPackage.kt index 5463faf66a8..16776573e76 100644 --- a/apps/mobile/android/app/src/main/java/com/uniswap/UniswapPackage.kt +++ b/apps/mobile/android/app/src/main/java/com/uniswap/UniswapPackage.kt @@ -6,6 +6,7 @@ import com.facebook.react.bridge.NativeModule import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.uimanager.ReactShadowNode import com.facebook.react.uimanager.ViewManager +import com.uniswap.notifications.SilentPushEventEmitterModule import com.uniswap.onboarding.backup.MnemonicConfirmationViewManager import com.uniswap.onboarding.backup.MnemonicDisplayViewManager import com.uniswap.onboarding.import.SeedPhraseInputViewManager @@ -28,5 +29,6 @@ class UniswapPackage : ReactPackage { RNEthersRSModule(reactContext), EmbeddedWalletModule(reactContext), ThemeModule(reactContext), + SilentPushEventEmitterModule(reactContext), ) } diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/notifications/SilentPushEventEmitterModule.kt b/apps/mobile/android/app/src/main/java/com/uniswap/notifications/SilentPushEventEmitterModule.kt new file mode 100644 index 00000000000..9c2504ec875 --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/uniswap/notifications/SilentPushEventEmitterModule.kt @@ -0,0 +1,129 @@ +package com.uniswap.notifications + +import android.util.Log +import androidx.annotation.Keep +import com.facebook.react.module.annotations.ReactModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.modules.core.DeviceEventManagerModule +import com.uniswap.utils.toWritableMap +import org.json.JSONObject + +@Keep +@ReactModule(name = SilentPushEventEmitterModule.MODULE_NAME) +class SilentPushEventEmitterModule( + reactContext: ReactApplicationContext +) : ReactContextBaseJavaModule(reactContext) { + + override fun getName() = MODULE_NAME + + override fun initialize() { + super.initialize() + instance = this + listenerCount = 0 + Log.d(TAG, "SilentPushEventEmitter initialized") + flushPendingEvents() + } + + override fun onCatalystInstanceDestroy() { + super.onCatalystInstanceDestroy() + if (instance === this) { + instance = null + } + listenerCount = 0 + } + + @ReactMethod + fun addListener(eventName: String) { + if (eventName != EVENT_NAME) { + return + } + listenerCount += 1 + flushPendingEvents() + } + + @ReactMethod + fun removeListeners(count: Int) { + if (count <= 0) { + return + } + listenerCount = (listenerCount - count).coerceAtLeast(0) + } + + private fun flushPendingEvents() { + if (!hasListeners()) { + return + } + + val events = synchronized(pendingPayloads) { + if (pendingPayloads.isEmpty()) { + null + } else { + val copy = ArrayList(pendingPayloads) + pendingPayloads.clear() + copy + } + } ?: return + + Log.d(TAG, "Flushing ${events.size} queued silent push events") + events.forEach { sendEvent(it) } + } + + private fun sendEvent(payload: JSONObject) { + if (!reactApplicationContext.hasActiveCatalystInstance()) { + synchronized(pendingPayloads) { + Log.d(TAG, "No active Catalyst instance; queueing payload: ${payload.toString()}") + pendingPayloads.add(JSONObject(payload.toString())) + } + return + } + + val map = payload.toWritableMap() + reactApplicationContext.runOnUiQueueThread { + if (!reactApplicationContext.hasActiveCatalystInstance()) { + synchronized(pendingPayloads) { + Log.d(TAG, "Catalyst inactive on UI thread; re-queueing payload: ${payload.toString()}") + pendingPayloads.add(JSONObject(payload.toString())) + } + return@runOnUiQueueThread + } + + Log.d(TAG, "Emitting silent push payload to JS: ${payload.toString()}") + reactApplicationContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) + .emit(EVENT_NAME, map) + } + } + + private fun hasListeners(): Boolean = instance != null && listenerCount > 0 + + companion object { + const val MODULE_NAME = "SilentPushEventEmitter" + private const val EVENT_NAME = "SilentPushReceived" + private const val TAG = "SilentPushEmitter" + private val pendingPayloads = mutableListOf() + + @Volatile + private var instance: SilentPushEventEmitterModule? = null + + @Volatile + private var listenerCount: Int = 0 + + fun emitEvent(payload: JSONObject?) { + val eventPayload = payload?.let { JSONObject(it.toString()) } ?: JSONObject() + val currentInstance = instance + + if (currentInstance != null && currentInstance.hasListeners()) { + Log.d(TAG, "Sending silent push event to JS immediately: $eventPayload") + currentInstance.sendEvent(eventPayload) + return + } + + synchronized(pendingPayloads) { + Log.d(TAG, "Queueing silent push payload until listeners attach: $eventPayload") + pendingPayloads.add(eventPayload) + } + } + } +} diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/notifications/SilentPushNotificationServiceExtension.kt b/apps/mobile/android/app/src/main/java/com/uniswap/notifications/SilentPushNotificationServiceExtension.kt new file mode 100644 index 00000000000..90321dc944d --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/uniswap/notifications/SilentPushNotificationServiceExtension.kt @@ -0,0 +1,123 @@ +package com.uniswap.notifications + +import android.util.Log +import androidx.annotation.Keep +import com.onesignal.notifications.INotification +import com.onesignal.notifications.INotificationReceivedEvent +import com.onesignal.notifications.INotificationServiceExtension +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.json.JSONException +import org.json.JSONObject + +@Keep +class SilentPushNotificationServiceExtension : INotificationServiceExtension { + override fun onNotificationReceived(event: INotificationReceivedEvent) { + val notification = event.notification + val payload = buildPayload(notification) + + val hasContentAvailableFlag = hasContentAvailable(payload) + val isMissingVisibleContent = notification.isMissingVisibleContent() + + Log.d( + TAG, + "Notification received. hasContentAvailable=$hasContentAvailableFlag, " + + "missingVisibleContent=$isMissingVisibleContent, payload=$payload", + ) + + if (!hasContentAvailableFlag && !isMissingVisibleContent) { + return + } + + Log.d(TAG, "Emitting silent push event: $payload") + val payloadForEmission = try { + JSONObject(payload.toString()) + } catch (error: JSONException) { + Log.w(TAG, "Failed to clone payload for emission: ${error.message}") + payload + } + + CoroutineScope(Dispatchers.Default).launch { + withContext(Dispatchers.Main) { + SilentPushEventEmitterModule.emitEvent(payloadForEmission) + } + } + + if (isMissingVisibleContent) { + event.preventDefault() + } + } + + private fun INotification.isMissingVisibleContent(): Boolean { + val title: String? = this.title + val body: String? = this.body + return title.isNullOrBlank() && body.isNullOrBlank() + } + + private fun buildPayload(notification: INotification): JSONObject { + val rawPayload = notification.rawPayload + val payload = try { + if (rawPayload.isNullOrBlank()) JSONObject() else JSONObject(rawPayload) + } catch (error: JSONException) { + Log.w(TAG, "Failed parsing raw payload: ${error.message}") + JSONObject() + } + + notification.additionalData?.let { additionalData -> + try { + payload.put("additionalData", additionalData) + } catch (error: JSONException) { + Log.w(TAG, "Failed to append additional data: ${error.message}") + } + } + + return payload + } + + private fun hasContentAvailable(payload: JSONObject?): Boolean { + if (payload == null) { + return false + } + + if (payload.hasContentAvailableFlag()) { + return true + } + + val aps = payload.optJSONObject("aps") + if (aps != null && aps.hasContentAvailableFlag()) { + return true + } + + val additionalData = payload.optJSONObject("additionalData") + if (additionalData != null && additionalData.hasContentAvailableFlag()) { + return true + } + + return false + } + + private fun JSONObject.hasContentAvailableFlag(): Boolean { + return opt(CONTENT_AVAILABLE_UNDERSCORE).isTruthy() || opt(CONTENT_AVAILABLE_HYPHEN).isTruthy() + } + + private fun Any?.isTruthy(): Boolean { + return when (this) { + null, JSONObject.NULL -> false + is Boolean -> this + is Int -> this == 1 + is Long -> this == 1L + is Double -> this == 1.0 + is Float -> this == 1f + is String -> equals("1", ignoreCase = true) || equals("true", ignoreCase = true) + else -> false + } + } + + companion object { + private const val TAG = "SilentPushExt" + private const val CONTENT_AVAILABLE_UNDERSCORE = "content_available" + private const val CONTENT_AVAILABLE_HYPHEN = "content-available" + } +} diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/utils/JsonWritableExtensions.kt b/apps/mobile/android/app/src/main/java/com/uniswap/utils/JsonWritableExtensions.kt new file mode 100644 index 00000000000..81415055e6d --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/uniswap/utils/JsonWritableExtensions.kt @@ -0,0 +1,61 @@ +package com.uniswap.utils + +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.WritableArray +import com.facebook.react.bridge.WritableMap +import org.json.JSONArray +import org.json.JSONObject + +fun JSONObject.toWritableMap(): WritableMap { + val map = Arguments.createMap() + val iterator = keys() + while (iterator.hasNext()) { + val key = iterator.next() + when (val value = opt(key)) { + null, JSONObject.NULL -> map.putNull(key) + is JSONObject -> map.putMap(key, value.toWritableMap()) + is JSONArray -> map.putArray(key, value.toWritableArray()) + is Boolean -> map.putBoolean(key, value) + is Int -> map.putInt(key, value) + is Long -> { + if (value in Int.MIN_VALUE..Int.MAX_VALUE) { + map.putInt(key, value.toInt()) + } else { + map.putDouble(key, value.toDouble()) + } + } + is Double -> map.putDouble(key, value) + is Float -> map.putDouble(key, value.toDouble()) + is Number -> map.putDouble(key, value.toDouble()) + is String -> map.putString(key, value) + else -> map.putString(key, value.toString()) + } + } + return map +} + +fun JSONArray.toWritableArray(): WritableArray { + val array = Arguments.createArray() + for (index in 0 until length()) { + when (val value = opt(index)) { + null, JSONObject.NULL -> array.pushNull() + is JSONObject -> array.pushMap(value.toWritableMap()) + is JSONArray -> array.pushArray(value.toWritableArray()) + is Boolean -> array.pushBoolean(value) + is Int -> array.pushInt(value) + is Long -> { + if (value in Int.MIN_VALUE..Int.MAX_VALUE) { + array.pushInt(value.toInt()) + } else { + array.pushDouble(value.toDouble()) + } + } + is Double -> array.pushDouble(value) + is Float -> array.pushDouble(value.toDouble()) + is Number -> array.pushDouble(value.toDouble()) + is String -> array.pushString(value) + else -> array.pushString(value.toString()) + } + } + return array +} diff --git a/apps/mobile/android/build.gradle b/apps/mobile/android/build.gradle index 9ca0ef7eecb..5aa41d8be2a 100644 --- a/apps/mobile/android/build.gradle +++ b/apps/mobile/android/build.gradle @@ -2,11 +2,6 @@ buildscript { ext { - buildToolsVersion = "35.0.0" - minSdkVersion = 28 - compileSdkVersion = 35 - targetSdkVersion = 35 - ndkVersion = "27.1.12297006" kotlinVersion = "2.0.21" appCompat = "1.6.1" @@ -36,6 +31,14 @@ plugins { id 'org.jetbrains.kotlin.plugin.compose' version "$kotlinVersion" apply false } +def reactNativeAndroidDir = new File( + providers.exec { + workingDir(rootDir) + commandLine("node", "--print", "require.resolve('react-native/package.json')") + }.standardOutput.asText.get().trim(), + "../android" +) + allprojects { project.pluginManager.withPlugin("com.facebook.react") { react { @@ -45,11 +48,15 @@ allprojects { } repositories { - maven { - // expo-camera bundles a custom com.google.android:cameraview - url "$rootDir/../../../node_modules/expo-camera/android/maven" - } + google() + mavenCentral() + maven { url 'https://www.jitpack.io' } + // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm + maven { url(reactNativeAndroidDir) } + // expo-camera bundles a custom com.google.android:cameraview + maven { url "$rootDir/../../../node_modules/expo-camera/android/maven" } } } +apply plugin: "expo-root-project" apply plugin: "com.facebook.react.rootproject" diff --git a/apps/mobile/android/gradle/wrapper/gradle-wrapper.properties b/apps/mobile/android/gradle/wrapper/gradle-wrapper.properties index d5edd72a874..4dd5cf37e34 100644 --- a/apps/mobile/android/gradle/wrapper/gradle-wrapper.properties +++ b/apps/mobile/android/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ #Wed April 10 16:30:26 EDT 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/apps/mobile/android/gradlew b/apps/mobile/android/gradlew index 98d9216fb08..faf93008b77 100755 --- a/apps/mobile/android/gradlew +++ b/apps/mobile/android/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,11 +15,53 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME @@ -29,158 +71,181 @@ app_path=$0 # Need this for daisy-chained symlinks. while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] do - ls=$(ls -ld "$app_path") - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s' "$PWD" ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum -warn() { - echo "$*" -} +warn () { + echo "$*" +} >&2 -die() { - echo - echo "$*" - echo - exit 1 -} +die () { + echo + echo "$*" + echo + exit 1 +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "$(uname)" in -CYGWIN*) - cygwin=true - ;; -Darwin*) - darwin=true - ;; -MINGW*) - msys=true - ;; -NONSTOP*) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ]; then - if [ -x "$JAVA_HOME/jre/sh/java" ]; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ]; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." - fi + fi else - JAVACMD="java" - if ! command -v java >/dev/null 2>&1; then - die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." - fi + fi fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ]; then - MAX_FD_LIMIT=$(ulimit -H -n) - if [ $? -eq 0 ]; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ]; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ]; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ]; then - APP_HOME=$(cygpath --path --mixed "$APP_HOME") - CLASSPATH=$(cygpath --path --mixed "$CLASSPATH") - - JAVACMD=$(cygpath --unix "$JAVACMD") - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=$(find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null) - SEP="" - for dir in $ROOTDIRSRAW; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ]; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@"; do - CHECK=$(echo "$arg" | egrep -c "$OURCYGPATTERN" -) - CHECK2=$(echo "$arg" | egrep -c "^-") ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ]; then ### Added a condition - eval $(echo args$i)=$(cygpath --path --ignore --mixed "$arg") - else - eval $(echo args$i)="\"$arg\"" - fi - i=$(expr $i + 1) - done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" fi -# Escape application args -save() { - for i; do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/"; done - echo " " -} -APP_ARGS=$(save "$@") +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/apps/mobile/android/settings.gradle b/apps/mobile/android/settings.gradle index 663fcfbe17b..8d322bc3be2 100644 --- a/apps/mobile/android/settings.gradle +++ b/apps/mobile/android/settings.gradle @@ -1,10 +1,35 @@ -pluginManagement { includeBuild("../../../node_modules/@react-native/gradle-plugin") } -plugins { id("com.facebook.react.settings") } -extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand(['bunx', 'rnef', 'config', '-p', 'android']) } +pluginManagement { + def reactNativeGradlePlugin = new File( + providers.exec { + workingDir(rootDir) + commandLine("node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })") + }.standardOutput.asText.get().trim() + ).getParentFile().absolutePath + includeBuild(reactNativeGradlePlugin) + + def expoPluginsPath = new File( + providers.exec { + workingDir(rootDir) + commandLine("node", "--print", "require.resolve('expo-modules-autolinking/package.json', { paths: [require.resolve('expo/package.json')] })") + }.standardOutput.asText.get().trim(), + "../android/expo-gradle-plugin" + ).absolutePath + includeBuild(expoPluginsPath) +} + +plugins { + id("com.facebook.react.settings") + id("expo-autolinking-settings") +} + +extensions.configure(com.facebook.react.ReactSettingsExtension) { ex -> + ex.autolinkLibrariesFromCommand(expoAutolinking.rnConfigCommand) +} + +expoAutolinking.useExpoModules() rootProject.name = 'Uniswap' -include ':app' -includeBuild('../../../node_modules/@react-native/gradle-plugin') -apply from: new File(["node", "--print", "require.resolve('../../../node_modules/expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle") -useExpoModules() +expoAutolinking.useExpoVersionCatalog() +includeBuild(expoAutolinking.reactNativeGradlePlugin) +include ':app' diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts new file mode 100644 index 00000000000..03aa4e6f462 --- /dev/null +++ b/apps/mobile/app.config.ts @@ -0,0 +1,18 @@ +import { ExpoConfig } from 'expo/config' + +const config: ExpoConfig = { + name: 'Uniswap', + slug: 'uniswapmobile', + scheme: 'uniswap', + owner: 'uniswap', + extra: { + eas: { + projectId: 'f1be3813-43d7-49ac-a792-7f42cf8500f5', + }, + }, + experiments: { + buildCacheProvider: 'eas', + }, +} + +export default config diff --git a/apps/mobile/app.json b/apps/mobile/app.json deleted file mode 100644 index b9b05c82db4..00000000000 --- a/apps/mobile/app.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "Uniswap", - "displayName": "Uniswap", - "appStoreUrl": "https://apps.apple.com/app/apple-store/id6443944476", - "playStoreUrl": "https://play.google.com/store/apps/details?id=com.uniswap.mobile", - "expo": { - "ios": { - "infoPlist": { - "CFBundleAllowMixedLocalizations": true - } - }, - "plugins": ["expo-localization", "expo-camera", "expo-local-authentication", "expo-secure-store"] - } -} diff --git a/apps/mobile/eas.json b/apps/mobile/eas.json new file mode 100644 index 00000000000..081b1650bfb --- /dev/null +++ b/apps/mobile/eas.json @@ -0,0 +1,72 @@ +{ + "cli": { + "version": ">= 15.0.15", + "appVersionSource": "remote" + }, + "build": { + "development": { + "bun": "1.3.1", + "node": "22.13.1", + "developmentClient": true, + "distribution": "internal", + "android": { + "buildType": "apk", + "gradleCommand": ":app:assembleDevDebug" + } + }, + "development-simulator": { + "bun": "1.3.1", + "node": "22.13.1", + "developmentClient": true, + "distribution": "internal", + "android": { + "buildType": "apk", + "gradleCommand": ":app:assembleDevDebug", + "withoutCredentials": true + }, + "ios": { + "simulator": true, + "withoutCredentials": true + } + }, + "development-release": { + "bun": "1.3.1", + "node": "22.13.1", + "distribution": "internal", + "android": { + "buildType": "apk", + "gradleCommand": ":app:assembleDevRelease" + } + }, + "beta": { + "bun": "1.3.1", + "node": "22.13.1", + "distribution": "internal", + "android": { + "buildType": "apk", + "gradleCommand": ":app:assembleBetaDebug" + } + }, + "beta-release": { + "bun": "1.3.1", + "node": "22.13.1", + "distribution": "internal", + "android": { + "buildType": "apk", + "gradleCommand": ":app:assembleBetaRelease" + } + }, + "production": { + "bun": "1.3.1", + "node": "22.13.1", + "autoIncrement": true, + "android": { + "buildType": "apk", + "gradleCommand": ":app:assembleProdRelease" + } + } + }, + "submit": { + "production": {} + } +} diff --git a/apps/mobile/fingerprint.config.js b/apps/mobile/fingerprint.config.js new file mode 100644 index 00000000000..0f3617ae437 --- /dev/null +++ b/apps/mobile/fingerprint.config.js @@ -0,0 +1,8 @@ +/** @type {import('@expo/fingerprint').Config} */ +const config = { + sourceSkips: [ + 'PackageJsonScriptsAll', // Skip all package.json scripts + 'ExpoConfigVersions', // Skip version bumps if you want + ], +} +module.exports = config diff --git a/apps/mobile/index.js b/apps/mobile/index.js index 46c83394bd3..d587e067bfc 100644 --- a/apps/mobile/index.js +++ b/apps/mobile/index.js @@ -14,6 +14,6 @@ import 'src/logbox' import 'src/polyfills' // biome-ignore assist/source/organizeImports: we want to keep the import order import App from 'src/app/App' -import { name as appName } from './app.json' +import AppConfig from './app.config' -AppRegistry.registerComponent(appName, () => App) +AppRegistry.registerComponent(AppConfig.name, () => App) diff --git a/apps/mobile/ios/Podfile b/apps/mobile/ios/Podfile index 7add18aba38..05b0c427464 100644 --- a/apps/mobile/ios/Podfile +++ b/apps/mobile/ios/Podfile @@ -26,11 +26,27 @@ target 'Uniswap' do Pod::UI.warn e end end - use_expo_modules!(exclude: ['expo-constants','expo-file-system', 'expo-font', 'expo-keep-awake', 'expo-error-recovery']) - config = use_native_modules!(['bunx', 'rnef', 'config', '-p', 'ios']) + + if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1' + config_command = ['node', '-e', "process.argv=['', '', 'config'];require('@react-native-community/cli').run()"]; + else + config_command = [ + 'node', + '--no-warnings', + '--eval', + 'require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'expo/package.json\')] }))(process.argv.slice(1))', + 'react-native-config', + '--json', + '--platform', + 'ios' + ] + end + + config = use_native_modules!(config_command) use_react_native!( :path => config[:reactNativePath], + :fabric_enabled => false, # to enable hermes on iOS, change `false` to `true` and then install pods :hermes_enabled => true ) diff --git a/apps/mobile/ios/Podfile.lock b/apps/mobile/ios/Podfile.lock index 41bed31112c..10cf77ed1a6 100644 --- a/apps/mobile/ios/Podfile.lock +++ b/apps/mobile/ios/Podfile.lock @@ -1161,60 +1161,326 @@ PODS: - BoringSSL-GRPC/Implementation (0.0.36): - BoringSSL-GRPC/Interface (= 0.0.36) - BoringSSL-GRPC/Interface (0.0.36) - - DatadogCore (2.27.0): - - DatadogInternal (= 2.27.0) - - DatadogCrashReporting (2.27.0): - - DatadogInternal (= 2.27.0) + - DatadogCore (2.30.0): + - DatadogInternal (= 2.30.0) + - DatadogCrashReporting (2.30.0): + - DatadogInternal (= 2.30.0) - PLCrashReporter (~> 1.12.0) - - DatadogInternal (2.27.0) - - DatadogLogs (2.27.0): - - DatadogInternal (= 2.27.0) - - DatadogRUM (2.27.0): - - DatadogInternal (= 2.27.0) - - DatadogSDKReactNative (2.8.2): - - DatadogCore (~> 2.27.0) - - DatadogCrashReporting (~> 2.27.0) - - DatadogLogs (~> 2.27.0) - - DatadogRUM (~> 2.27.0) - - DatadogTrace (~> 2.27.0) - - DatadogWebViewTracking (~> 2.27.0) - - React-Core - - DatadogTrace (2.27.0): - - DatadogInternal (= 2.27.0) + - DatadogInternal (2.30.0) + - DatadogLogs (2.30.0): + - DatadogInternal (= 2.30.0) + - DatadogRUM (2.30.0): + - DatadogInternal (= 2.30.0) + - DatadogSDKReactNative (2.12.2): + - DatadogCore (= 2.30.0) + - DatadogCrashReporting (= 2.30.0) + - DatadogLogs (= 2.30.0) + - DatadogRUM (= 2.30.0) + - DatadogTrace (= 2.30.0) + - DatadogWebViewTracking (= 2.30.0) + - React-Core + - DatadogTrace (2.30.0): + - DatadogInternal (= 2.30.0) - OpenTelemetrySwiftApi (= 1.13.1) - - DatadogWebViewTracking (2.27.0): - - DatadogInternal (= 2.27.0) + - DatadogWebViewTracking (2.30.0): + - DatadogInternal (= 2.30.0) - DoubleConversion (1.1.6) - EthersRS (0.0.5) - - Expo (52.0.46): + - EXConstants (17.1.7): + - ExpoModulesCore + - EXJSONUtils (0.15.0) + - EXManifests (0.16.6): + - ExpoModulesCore + - Expo (53.0.22): + - DoubleConversion + - ExpoModulesCore + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTAppDelegate + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactAppDependencyProvider + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - expo-dev-client (5.2.4): + - EXManifests + - expo-dev-launcher + - expo-dev-menu + - expo-dev-menu-interface + - EXUpdatesInterface + - expo-dev-launcher (5.1.16): + - DoubleConversion + - EXManifests + - expo-dev-launcher/Main (= 5.1.16) + - expo-dev-menu + - expo-dev-menu-interface + - ExpoModulesCore + - EXUpdatesInterface + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-jsinspector + - React-NativeModulesApple + - React-RCTAppDelegate + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactAppDependencyProvider + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - expo-dev-launcher/Main (5.1.16): + - DoubleConversion + - EXManifests + - expo-dev-launcher/Unsafe + - expo-dev-menu + - expo-dev-menu-interface - ExpoModulesCore - - ExpoAsset (11.0.5): + - EXUpdatesInterface + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-jsinspector + - React-NativeModulesApple + - React-RCTAppDelegate + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactAppDependencyProvider + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - expo-dev-launcher/Unsafe (5.1.16): + - DoubleConversion + - EXManifests + - expo-dev-menu + - expo-dev-menu-interface - ExpoModulesCore - - ExpoBlur (14.0.3): + - EXUpdatesInterface + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-jsinspector + - React-NativeModulesApple + - React-RCTAppDelegate + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactAppDependencyProvider + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - expo-dev-menu (6.1.14): + - DoubleConversion + - expo-dev-menu/Main (= 6.1.14) + - expo-dev-menu/ReactNativeCompatibles (= 6.1.14) + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - expo-dev-menu-interface (1.10.0) + - expo-dev-menu/Main (6.1.14): + - DoubleConversion + - EXManifests + - expo-dev-menu-interface + - expo-dev-menu/Vendored - ExpoModulesCore - - ExpoCamera (16.0.18): + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-jsinspector + - React-NativeModulesApple + - React-RCTFabric + - React-rendererconsistency + - React-renderercss + - React-rendererdebug + - React-utils + - ReactAppDependencyProvider + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - expo-dev-menu/ReactNativeCompatibles (6.1.14): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - expo-dev-menu/SafeAreaView (6.1.14): + - DoubleConversion + - ExpoModulesCore + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - expo-dev-menu/Vendored (6.1.14): + - DoubleConversion + - expo-dev-menu/SafeAreaView + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - ExpoAsset (11.1.7): + - ExpoModulesCore + - ExpoBlur (14.1.5): + - ExpoModulesCore + - ExpoCamera (16.1.11): - ExpoModulesCore - ZXingObjC/OneD - ZXingObjC/PDF417 - - ExpoClipboard (7.0.1): + - ExpoClipboard (7.1.5): - ExpoModulesCore - - ExpoFileSystem (18.0.12): + - ExpoFileSystem (18.1.11): - ExpoModulesCore - - ExpoFont (13.0.4): + - ExpoFont (13.3.2): - ExpoModulesCore - ExpoHaptics (14.0.1): - ExpoModulesCore - - ExpoKeepAwake (14.0.3): + - ExpoKeepAwake (14.1.4): - ExpoModulesCore - - ExpoLinearGradient (14.0.2): + - ExpoLinearGradient (14.1.5): - ExpoModulesCore - - ExpoLinking (7.0.5): + - ExpoLinking (7.1.7): - ExpoModulesCore - - ExpoLocalAuthentication (15.0.2): + - ExpoLocalAuthentication (16.0.5): - ExpoModulesCore - - ExpoLocalization (16.0.1): + - ExpoLocalization (16.1.6): - ExpoModulesCore - - ExpoModulesCore (2.2.3): + - ExpoModulesCore (2.5.0): - DoubleConversion - glog - hermes-engine @@ -1226,28 +1492,31 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-ImageManager + - React-jsi - React-jsinspector - React-NativeModulesApple - - React-RCTAppDelegate - React-RCTFabric + - React-renderercss - React-rendererdebug - React-utils - - ReactAppDependencyProvider - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - ExpoScreenCapture (7.0.1): + - ExpoScreenCapture (7.2.0): - ExpoModulesCore - ExpoSecureStore (14.0.1): - ExpoModulesCore - - ExpoStoreReview (8.0.1): + - ExpoStoreReview (8.1.5): + - ExpoModulesCore + - ExpoWebBrowser (14.2.0): - ExpoModulesCore - - ExpoWebBrowser (14.0.2): + - EXUpdatesInterface (1.1.0): - ExpoModulesCore - fast_float (6.1.4) - - FBLazyVector (0.77.2) + - FBLazyVector (0.79.5) - Firebase/Auth (11.2.0): - Firebase/CoreOnly - FirebaseAuth (~> 11.2.0) @@ -1256,7 +1525,7 @@ PODS: - Firebase/Firestore (11.2.0): - Firebase/CoreOnly - FirebaseFirestore (~> 11.2.0) - - FirebaseAppCheckInterop (11.14.0) + - FirebaseAppCheckInterop (11.15.0) - FirebaseAuth (11.2.0): - FirebaseAppCheckInterop (~> 11.0) - FirebaseAuthInterop (~> 11.0) @@ -1266,14 +1535,14 @@ PODS: - GoogleUtilities/Environment (~> 8.0) - GTMSessionFetcher/Core (~> 3.4) - RecaptchaInterop (~> 100.0) - - FirebaseAuthInterop (11.14.0) + - FirebaseAuthInterop (11.15.0) - FirebaseCore (11.2.0): - FirebaseCoreInternal (~> 11.0) - GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/Logger (~> 8.0) - FirebaseCoreExtension (11.4.1): - FirebaseCore (~> 11.0) - - FirebaseCoreInternal (11.14.0): + - FirebaseCoreInternal (11.15.0): - "GoogleUtilities/NSData+zlib (~> 8.1)" - FirebaseFirestore (11.2.0): - FirebaseCore (~> 11.0) @@ -1295,7 +1564,7 @@ PODS: - gRPC-Core (~> 1.65.0) - leveldb-library (~> 1.22) - nanopb (~> 3.30910.0) - - FirebaseSharedSwift (11.14.0) + - FirebaseSharedSwift (11.15.0) - fmt (11.0.2) - glog (0.3.5) - GoogleUtilities/AppDelegateSwizzler (8.1.0): @@ -1410,9 +1679,9 @@ PODS: - gRPC-Core/Interface (1.65.5) - gRPC-Core/Privacy (1.65.5) - GTMSessionFetcher/Core (3.5.0) - - hermes-engine (0.77.2): - - hermes-engine/Pre-built (= 0.77.2) - - hermes-engine/Pre-built (0.77.2) + - hermes-engine (0.79.5): + - hermes-engine/Pre-built (= 0.79.5) + - hermes-engine/Pre-built (0.79.5) - leveldb-library (1.22.6) - libwebp (1.5.0): - libwebp/demux (= 1.5.0) @@ -1426,9 +1695,9 @@ PODS: - libwebp/sharpyuv (1.5.0) - libwebp/webp (1.5.0): - libwebp/sharpyuv - - MMKV (2.2.2): - - MMKVCore (~> 2.2.2) - - MMKVCore (2.2.2) + - MMKV (2.2.4): + - MMKVCore (~> 2.2.4) + - MMKVCore (2.2.4) - nanopb (3.30910.0): - nanopb/decode (= 3.30910.0) - nanopb/encode (= 3.30910.0) @@ -1501,44 +1770,45 @@ PODS: - fast_float (= 6.1.4) - fmt (= 11.0.2) - glog - - RCTDeprecation (0.77.2) - - RCTRequired (0.77.2) - - RCTTypeSafety (0.77.2): - - FBLazyVector (= 0.77.2) - - RCTRequired (= 0.77.2) - - React-Core (= 0.77.2) - - React (0.77.2): - - React-Core (= 0.77.2) - - React-Core/DevSupport (= 0.77.2) - - React-Core/RCTWebSocket (= 0.77.2) - - React-RCTActionSheet (= 0.77.2) - - React-RCTAnimation (= 0.77.2) - - React-RCTBlob (= 0.77.2) - - React-RCTImage (= 0.77.2) - - React-RCTLinking (= 0.77.2) - - React-RCTNetwork (= 0.77.2) - - React-RCTSettings (= 0.77.2) - - React-RCTText (= 0.77.2) - - React-RCTVibration (= 0.77.2) - - React-callinvoker (0.77.2) - - React-Core (0.77.2): + - RCTDeprecation (0.79.5) + - RCTRequired (0.79.5) + - RCTTypeSafety (0.79.5): + - FBLazyVector (= 0.79.5) + - RCTRequired (= 0.79.5) + - React-Core (= 0.79.5) + - React (0.79.5): + - React-Core (= 0.79.5) + - React-Core/DevSupport (= 0.79.5) + - React-Core/RCTWebSocket (= 0.79.5) + - React-RCTActionSheet (= 0.79.5) + - React-RCTAnimation (= 0.79.5) + - React-RCTBlob (= 0.79.5) + - React-RCTImage (= 0.79.5) + - React-RCTLinking (= 0.79.5) + - React-RCTNetwork (= 0.79.5) + - React-RCTSettings (= 0.79.5) + - React-RCTText (= 0.79.5) + - React-RCTVibration (= 0.79.5) + - React-callinvoker (0.79.5) + - React-Core (0.79.5): - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) - RCTDeprecation - - React-Core/Default (= 0.77.2) + - React-Core/Default (= 0.79.5) - React-cxxreact - React-featureflags - React-hermes - React-jsi - React-jsiexecutor - React-jsinspector + - React-jsitooling - React-perflogger - React-runtimescheduler - React-utils - SocketRocket (= 0.7.1) - Yoga - - React-Core/CoreModulesHeaders (0.77.2): + - React-Core/CoreModulesHeaders (0.79.5): - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) @@ -1550,12 +1820,13 @@ PODS: - React-jsi - React-jsiexecutor - React-jsinspector + - React-jsitooling - React-perflogger - React-runtimescheduler - React-utils - SocketRocket (= 0.7.1) - Yoga - - React-Core/Default (0.77.2): + - React-Core/Default (0.79.5): - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) @@ -1566,30 +1837,32 @@ PODS: - React-jsi - React-jsiexecutor - React-jsinspector + - React-jsitooling - React-perflogger - React-runtimescheduler - React-utils - SocketRocket (= 0.7.1) - Yoga - - React-Core/DevSupport (0.77.2): + - React-Core/DevSupport (0.79.5): - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) - RCTDeprecation - - React-Core/Default (= 0.77.2) - - React-Core/RCTWebSocket (= 0.77.2) + - React-Core/Default (= 0.79.5) + - React-Core/RCTWebSocket (= 0.79.5) - React-cxxreact - React-featureflags - React-hermes - React-jsi - React-jsiexecutor - React-jsinspector + - React-jsitooling - React-perflogger - React-runtimescheduler - React-utils - SocketRocket (= 0.7.1) - Yoga - - React-Core/RCTActionSheetHeaders (0.77.2): + - React-Core/RCTActionSheetHeaders (0.79.5): - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) @@ -1601,12 +1874,13 @@ PODS: - React-jsi - React-jsiexecutor - React-jsinspector + - React-jsitooling - React-perflogger - React-runtimescheduler - React-utils - SocketRocket (= 0.7.1) - Yoga - - React-Core/RCTAnimationHeaders (0.77.2): + - React-Core/RCTAnimationHeaders (0.79.5): - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) @@ -1618,12 +1892,13 @@ PODS: - React-jsi - React-jsiexecutor - React-jsinspector + - React-jsitooling - React-perflogger - React-runtimescheduler - React-utils - SocketRocket (= 0.7.1) - Yoga - - React-Core/RCTBlobHeaders (0.77.2): + - React-Core/RCTBlobHeaders (0.79.5): - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) @@ -1635,12 +1910,13 @@ PODS: - React-jsi - React-jsiexecutor - React-jsinspector + - React-jsitooling - React-perflogger - React-runtimescheduler - React-utils - SocketRocket (= 0.7.1) - Yoga - - React-Core/RCTImageHeaders (0.77.2): + - React-Core/RCTImageHeaders (0.79.5): - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) @@ -1652,12 +1928,13 @@ PODS: - React-jsi - React-jsiexecutor - React-jsinspector + - React-jsitooling - React-perflogger - React-runtimescheduler - React-utils - SocketRocket (= 0.7.1) - Yoga - - React-Core/RCTLinkingHeaders (0.77.2): + - React-Core/RCTLinkingHeaders (0.79.5): - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) @@ -1669,12 +1946,13 @@ PODS: - React-jsi - React-jsiexecutor - React-jsinspector + - React-jsitooling - React-perflogger - React-runtimescheduler - React-utils - SocketRocket (= 0.7.1) - Yoga - - React-Core/RCTNetworkHeaders (0.77.2): + - React-Core/RCTNetworkHeaders (0.79.5): - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) @@ -1686,12 +1964,13 @@ PODS: - React-jsi - React-jsiexecutor - React-jsinspector + - React-jsitooling - React-perflogger - React-runtimescheduler - React-utils - SocketRocket (= 0.7.1) - Yoga - - React-Core/RCTSettingsHeaders (0.77.2): + - React-Core/RCTSettingsHeaders (0.79.5): - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) @@ -1703,12 +1982,13 @@ PODS: - React-jsi - React-jsiexecutor - React-jsinspector + - React-jsitooling - React-perflogger - React-runtimescheduler - React-utils - SocketRocket (= 0.7.1) - Yoga - - React-Core/RCTTextHeaders (0.77.2): + - React-Core/RCTTextHeaders (0.79.5): - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) @@ -1720,12 +2000,13 @@ PODS: - React-jsi - React-jsiexecutor - React-jsinspector + - React-jsitooling - React-perflogger - React-runtimescheduler - React-utils - SocketRocket (= 0.7.1) - Yoga - - React-Core/RCTVibrationHeaders (0.77.2): + - React-Core/RCTVibrationHeaders (0.79.5): - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) @@ -1737,44 +2018,47 @@ PODS: - React-jsi - React-jsiexecutor - React-jsinspector + - React-jsitooling - React-perflogger - React-runtimescheduler - React-utils - SocketRocket (= 0.7.1) - Yoga - - React-Core/RCTWebSocket (0.77.2): + - React-Core/RCTWebSocket (0.79.5): - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) - RCTDeprecation - - React-Core/Default (= 0.77.2) + - React-Core/Default (= 0.79.5) - React-cxxreact - React-featureflags - React-hermes - React-jsi - React-jsiexecutor - React-jsinspector + - React-jsitooling - React-perflogger - React-runtimescheduler - React-utils - SocketRocket (= 0.7.1) - Yoga - - React-CoreModules (0.77.2): + - React-CoreModules (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) - RCT-Folly (= 2024.11.18.00) - - RCTTypeSafety (= 0.77.2) - - React-Core/CoreModulesHeaders (= 0.77.2) - - React-jsi (= 0.77.2) + - RCTTypeSafety (= 0.79.5) + - React-Core/CoreModulesHeaders (= 0.79.5) + - React-jsi (= 0.79.5) - React-jsinspector + - React-jsinspectortracing - React-NativeModulesApple - React-RCTBlob - React-RCTFBReactNativeSpec - - React-RCTImage (= 0.77.2) + - React-RCTImage (= 0.79.5) - ReactCommon - SocketRocket (= 0.7.1) - - React-cxxreact (0.77.2): + - React-cxxreact (0.79.5): - boost - DoubleConversion - fast_float (= 6.1.4) @@ -1782,37 +2066,40 @@ PODS: - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) - - React-callinvoker (= 0.77.2) - - React-debug (= 0.77.2) - - React-jsi (= 0.77.2) + - React-callinvoker (= 0.79.5) + - React-debug (= 0.79.5) + - React-jsi (= 0.79.5) - React-jsinspector - - React-logger (= 0.77.2) - - React-perflogger (= 0.77.2) - - React-runtimeexecutor (= 0.77.2) - - React-timing (= 0.77.2) - - React-debug (0.77.2) - - React-defaultsnativemodule (0.77.2): + - React-jsinspectortracing + - React-logger (= 0.79.5) + - React-perflogger (= 0.79.5) + - React-runtimeexecutor (= 0.79.5) + - React-timing (= 0.79.5) + - React-debug (0.79.5) + - React-defaultsnativemodule (0.79.5): - hermes-engine - RCT-Folly - React-domnativemodule - React-featureflagsnativemodule + - React-hermes - React-idlecallbacksnativemodule - React-jsi - React-jsiexecutor - React-microtasksnativemodule - React-RCTFBReactNativeSpec - - React-domnativemodule (0.77.2): + - React-domnativemodule (0.79.5): - hermes-engine - RCT-Folly - React-Fabric - React-FabricComponents - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-RCTFBReactNativeSpec - ReactCommon/turbomodule/core - Yoga - - React-Fabric (0.77.2): + - React-Fabric (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -1824,23 +2111,25 @@ PODS: - React-Core - React-cxxreact - React-debug - - React-Fabric/animations (= 0.77.2) - - React-Fabric/attributedstring (= 0.77.2) - - React-Fabric/componentregistry (= 0.77.2) - - React-Fabric/componentregistrynative (= 0.77.2) - - React-Fabric/components (= 0.77.2) - - React-Fabric/core (= 0.77.2) - - React-Fabric/dom (= 0.77.2) - - React-Fabric/imagemanager (= 0.77.2) - - React-Fabric/leakchecker (= 0.77.2) - - React-Fabric/mounting (= 0.77.2) - - React-Fabric/observers (= 0.77.2) - - React-Fabric/scheduler (= 0.77.2) - - React-Fabric/telemetry (= 0.77.2) - - React-Fabric/templateprocessor (= 0.77.2) - - React-Fabric/uimanager (= 0.77.2) + - React-Fabric/animations (= 0.79.5) + - React-Fabric/attributedstring (= 0.79.5) + - React-Fabric/componentregistry (= 0.79.5) + - React-Fabric/componentregistrynative (= 0.79.5) + - React-Fabric/components (= 0.79.5) + - React-Fabric/consistency (= 0.79.5) + - React-Fabric/core (= 0.79.5) + - React-Fabric/dom (= 0.79.5) + - React-Fabric/imagemanager (= 0.79.5) + - React-Fabric/leakchecker (= 0.79.5) + - React-Fabric/mounting (= 0.79.5) + - React-Fabric/observers (= 0.79.5) + - React-Fabric/scheduler (= 0.79.5) + - React-Fabric/telemetry (= 0.79.5) + - React-Fabric/templateprocessor (= 0.79.5) + - React-Fabric/uimanager (= 0.79.5) - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -1848,7 +2137,7 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/animations (0.77.2): + - React-Fabric/animations (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -1862,6 +2151,29 @@ PODS: - React-debug - React-featureflags - React-graphics + - React-hermes + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/attributedstring (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -1869,7 +2181,7 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/attributedstring (0.77.2): + - React-Fabric/componentregistry (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -1883,6 +2195,7 @@ PODS: - React-debug - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -1890,7 +2203,7 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/componentregistry (0.77.2): + - React-Fabric/componentregistrynative (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -1904,6 +2217,7 @@ PODS: - React-debug - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -1911,7 +2225,7 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/componentregistrynative (0.77.2): + - React-Fabric/components (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -1923,8 +2237,13 @@ PODS: - React-Core - React-cxxreact - React-debug + - React-Fabric/components/legacyviewmanagerinterop (= 0.79.5) + - React-Fabric/components/root (= 0.79.5) + - React-Fabric/components/scrollview (= 0.79.5) + - React-Fabric/components/view (= 0.79.5) - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -1932,7 +2251,7 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/components (0.77.2): + - React-Fabric/components/legacyviewmanagerinterop (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -1944,11 +2263,9 @@ PODS: - React-Core - React-cxxreact - React-debug - - React-Fabric/components/legacyviewmanagerinterop (= 0.77.2) - - React-Fabric/components/root (= 0.77.2) - - React-Fabric/components/view (= 0.77.2) - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -1956,7 +2273,7 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/components/legacyviewmanagerinterop (0.77.2): + - React-Fabric/components/root (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -1970,6 +2287,7 @@ PODS: - React-debug - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -1977,7 +2295,7 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/components/root (0.77.2): + - React-Fabric/components/scrollview (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -1991,6 +2309,7 @@ PODS: - React-debug - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -1998,7 +2317,7 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/components/view (0.77.2): + - React-Fabric/components/view (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2012,15 +2331,39 @@ PODS: - React-debug - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger + - React-renderercss - React-rendererdebug - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - Yoga - - React-Fabric/core (0.77.2): + - React-Fabric/consistency (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-hermes + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/core (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2034,6 +2377,7 @@ PODS: - React-debug - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2041,7 +2385,7 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/dom (0.77.2): + - React-Fabric/dom (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2055,6 +2399,7 @@ PODS: - React-debug - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2062,7 +2407,7 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/imagemanager (0.77.2): + - React-Fabric/imagemanager (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2076,6 +2421,7 @@ PODS: - React-debug - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2083,7 +2429,7 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/leakchecker (0.77.2): + - React-Fabric/leakchecker (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2097,6 +2443,7 @@ PODS: - React-debug - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2104,7 +2451,7 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/mounting (0.77.2): + - React-Fabric/mounting (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2118,6 +2465,7 @@ PODS: - React-debug - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2125,7 +2473,7 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/observers (0.77.2): + - React-Fabric/observers (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2137,9 +2485,10 @@ PODS: - React-Core - React-cxxreact - React-debug - - React-Fabric/observers/events (= 0.77.2) + - React-Fabric/observers/events (= 0.79.5) - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2147,7 +2496,7 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/observers/events (0.77.2): + - React-Fabric/observers/events (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2161,6 +2510,7 @@ PODS: - React-debug - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2168,7 +2518,7 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/scheduler (0.77.2): + - React-Fabric/scheduler (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2183,6 +2533,7 @@ PODS: - React-Fabric/observers/events - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2191,7 +2542,7 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/telemetry (0.77.2): + - React-Fabric/telemetry (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2205,6 +2556,7 @@ PODS: - React-debug - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2212,7 +2564,7 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/templateprocessor (0.77.2): + - React-Fabric/templateprocessor (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2226,6 +2578,7 @@ PODS: - React-debug - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2233,7 +2586,7 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/uimanager (0.77.2): + - React-Fabric/uimanager (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2245,9 +2598,10 @@ PODS: - React-Core - React-cxxreact - React-debug - - React-Fabric/uimanager/consistency (= 0.77.2) + - React-Fabric/uimanager/consistency (= 0.79.5) - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2256,7 +2610,7 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/uimanager/consistency (0.77.2): + - React-Fabric/uimanager/consistency (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2270,6 +2624,7 @@ PODS: - React-debug - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2278,7 +2633,7 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-FabricComponents (0.77.2): + - React-FabricComponents (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2291,10 +2646,11 @@ PODS: - React-cxxreact - React-debug - React-Fabric - - React-FabricComponents/components (= 0.77.2) - - React-FabricComponents/textlayoutmanager (= 0.77.2) + - React-FabricComponents/components (= 0.79.5) + - React-FabricComponents/textlayoutmanager (= 0.79.5) - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2303,7 +2659,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - Yoga - - React-FabricComponents/components (0.77.2): + - React-FabricComponents/components (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2316,17 +2672,18 @@ PODS: - React-cxxreact - React-debug - React-Fabric - - React-FabricComponents/components/inputaccessory (= 0.77.2) - - React-FabricComponents/components/iostextinput (= 0.77.2) - - React-FabricComponents/components/modal (= 0.77.2) - - React-FabricComponents/components/rncore (= 0.77.2) - - React-FabricComponents/components/safeareaview (= 0.77.2) - - React-FabricComponents/components/scrollview (= 0.77.2) - - React-FabricComponents/components/text (= 0.77.2) - - React-FabricComponents/components/textinput (= 0.77.2) - - React-FabricComponents/components/unimplementedview (= 0.77.2) + - React-FabricComponents/components/inputaccessory (= 0.79.5) + - React-FabricComponents/components/iostextinput (= 0.79.5) + - React-FabricComponents/components/modal (= 0.79.5) + - React-FabricComponents/components/rncore (= 0.79.5) + - React-FabricComponents/components/safeareaview (= 0.79.5) + - React-FabricComponents/components/scrollview (= 0.79.5) + - React-FabricComponents/components/text (= 0.79.5) + - React-FabricComponents/components/textinput (= 0.79.5) + - React-FabricComponents/components/unimplementedview (= 0.79.5) - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2335,7 +2692,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - Yoga - - React-FabricComponents/components/inputaccessory (0.77.2): + - React-FabricComponents/components/inputaccessory (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2350,6 +2707,7 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2358,7 +2716,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - Yoga - - React-FabricComponents/components/iostextinput (0.77.2): + - React-FabricComponents/components/iostextinput (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2373,6 +2731,7 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2381,7 +2740,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - Yoga - - React-FabricComponents/components/modal (0.77.2): + - React-FabricComponents/components/modal (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2396,6 +2755,7 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2404,7 +2764,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - Yoga - - React-FabricComponents/components/rncore (0.77.2): + - React-FabricComponents/components/rncore (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2419,6 +2779,7 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2427,7 +2788,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - Yoga - - React-FabricComponents/components/safeareaview (0.77.2): + - React-FabricComponents/components/safeareaview (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2442,6 +2803,7 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2450,7 +2812,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - Yoga - - React-FabricComponents/components/scrollview (0.77.2): + - React-FabricComponents/components/scrollview (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2465,6 +2827,7 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2473,7 +2836,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - Yoga - - React-FabricComponents/components/text (0.77.2): + - React-FabricComponents/components/text (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2488,6 +2851,7 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2496,7 +2860,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - Yoga - - React-FabricComponents/components/textinput (0.77.2): + - React-FabricComponents/components/textinput (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2511,6 +2875,7 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2519,7 +2884,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - Yoga - - React-FabricComponents/components/unimplementedview (0.77.2): + - React-FabricComponents/components/unimplementedview (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2534,6 +2899,7 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2542,7 +2908,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - Yoga - - React-FabricComponents/textlayoutmanager (0.77.2): + - React-FabricComponents/textlayoutmanager (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2557,6 +2923,7 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2565,66 +2932,74 @@ PODS: - React-utils - ReactCommon/turbomodule/core - Yoga - - React-FabricImage (0.77.2): + - React-FabricImage (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) - glog - hermes-engine - RCT-Folly/Fabric (= 2024.11.18.00) - - RCTRequired (= 0.77.2) - - RCTTypeSafety (= 0.77.2) + - RCTRequired (= 0.79.5) + - RCTTypeSafety (= 0.79.5) - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-ImageManager - React-jsi - - React-jsiexecutor (= 0.77.2) + - React-jsiexecutor (= 0.79.5) - React-logger - React-rendererdebug - React-utils - ReactCommon - Yoga - - React-featureflags (0.77.2) - - React-featureflagsnativemodule (0.77.2): + - React-featureflags (0.79.5): + - RCT-Folly (= 2024.11.18.00) + - React-featureflagsnativemodule (0.79.5): - hermes-engine - RCT-Folly - React-featureflags + - React-hermes - React-jsi - React-jsiexecutor - React-RCTFBReactNativeSpec - ReactCommon/turbomodule/core - - React-graphics (0.77.2): + - React-graphics (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) - glog + - hermes-engine - RCT-Folly/Fabric (= 2024.11.18.00) + - React-hermes - React-jsi - React-jsiexecutor - React-utils - - React-hermes (0.77.2): + - React-hermes (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) - - React-cxxreact (= 0.77.2) + - React-cxxreact (= 0.79.5) - React-jsi - - React-jsiexecutor (= 0.77.2) + - React-jsiexecutor (= 0.79.5) - React-jsinspector - - React-perflogger (= 0.77.2) + - React-jsinspectortracing + - React-perflogger (= 0.79.5) - React-runtimeexecutor - - React-idlecallbacksnativemodule (0.77.2): + - React-idlecallbacksnativemodule (0.79.5): + - glog - hermes-engine - RCT-Folly + - React-hermes - React-jsi - React-jsiexecutor - React-RCTFBReactNativeSpec - React-runtimescheduler - ReactCommon/turbomodule/core - - React-ImageManager (0.77.2): + - React-ImageManager (0.79.5): - glog - RCT-Folly/Fabric - React-Core/Default @@ -2633,7 +3008,7 @@ PODS: - React-graphics - React-rendererdebug - React-utils - - React-jserrorhandler (0.77.2): + - React-jserrorhandler (0.79.5): - glog - hermes-engine - RCT-Folly/Fabric (= 2024.11.18.00) @@ -2642,7 +3017,7 @@ PODS: - React-featureflags - React-jsi - ReactCommon/turbomodule/bridging - - React-jsi (0.77.2): + - React-jsi (0.79.5): - boost - DoubleConversion - fast_float (= 6.1.4) @@ -2650,36 +3025,52 @@ PODS: - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) - - React-jsiexecutor (0.77.2): + - React-jsiexecutor (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) - - React-cxxreact (= 0.77.2) - - React-jsi (= 0.77.2) + - React-cxxreact (= 0.79.5) + - React-jsi (= 0.79.5) - React-jsinspector - - React-perflogger (= 0.77.2) - - React-jsinspector (0.77.2): + - React-jsinspectortracing + - React-perflogger (= 0.79.5) + - React-jsinspector (0.79.5): - DoubleConversion - glog - hermes-engine - - RCT-Folly (= 2024.11.18.00) + - RCT-Folly - React-featureflags - React-jsi - - React-perflogger (= 0.77.2) - - React-runtimeexecutor (= 0.77.2) - - React-jsitracing (0.77.2): + - React-jsinspectortracing + - React-perflogger (= 0.79.5) + - React-runtimeexecutor (= 0.79.5) + - React-jsinspectortracing (0.79.5): + - RCT-Folly + - React-oscompat + - React-jsitooling (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - RCT-Folly (= 2024.11.18.00) + - React-cxxreact (= 0.79.5) + - React-jsi (= 0.79.5) + - React-jsinspector + - React-jsinspectortracing + - React-jsitracing (0.79.5): - React-jsi - - React-logger (0.77.2): + - React-logger (0.79.5): - glog - - React-Mapbuffer (0.77.2): + - React-Mapbuffer (0.79.5): - glog - React-debug - - React-microtasksnativemodule (0.77.2): + - React-microtasksnativemodule (0.79.5): - hermes-engine - RCT-Folly + - React-hermes - React-jsi - React-jsiexecutor - React-RCTFBReactNativeSpec @@ -2687,7 +3078,7 @@ PODS: - react-native-appsflyer (6.13.1): - AppsFlyerFramework (= 6.13.1) - React - - react-native-compat (2.21.4): + - react-native-compat (2.23.0): - DoubleConversion - glog - hermes-engine @@ -2699,9 +3090,12 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-ImageManager + - React-jsi - React-NativeModulesApple - React-RCTFabric + - React-renderercss - React-rendererdebug - React-utils - ReactCodegen @@ -2724,9 +3118,12 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-ImageManager + - React-jsi - React-NativeModulesApple - React-RCTFabric + - React-renderercss - React-rendererdebug - React-utils - ReactCodegen @@ -2745,9 +3142,12 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-ImageManager + - React-jsi - React-NativeModulesApple - React-RCTFabric + - React-renderercss - React-rendererdebug - React-utils - ReactCodegen @@ -2762,7 +3162,7 @@ PODS: - react-native-onesignal (5.2.9): - OneSignalXCFramework (= 5.2.10) - React (< 1.0.0, >= 0.13.0) - - react-native-pager-view (6.5.1): + - react-native-pager-view (6.7.1): - DoubleConversion - glog - hermes-engine @@ -2774,9 +3174,12 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-ImageManager + - React-jsi - React-NativeModulesApple - React-RCTFabric + - React-renderercss - React-rendererdebug - React-utils - ReactCodegen @@ -2787,9 +3190,9 @@ PODS: - React-Core - react-native-restart (0.0.27): - React-Core - - react-native-safe-area-context (5.1.0): + - react-native-safe-area-context (5.4.0): - React-Core - - react-native-skia (1.12.4): + - react-native-skia (2.2.4): - DoubleConversion - glog - hermes-engine @@ -2803,16 +3206,19 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-ImageManager + - React-jsi - React-NativeModulesApple - React-RCTFabric + - React-renderercss - React-rendererdebug - React-utils - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - react-native-slider (4.5.5): + - react-native-slider (4.5.6): - DoubleConversion - glog - hermes-engine @@ -2824,9 +3230,12 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-ImageManager + - React-jsi - React-NativeModulesApple - React-RCTFabric + - React-renderercss - React-rendererdebug - React-utils - ReactCodegen @@ -2845,10 +3254,13 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-ImageManager + - React-jsi - react-native-video/Video (= 6.13.0) - React-NativeModulesApple - React-RCTFabric + - React-renderercss - React-rendererdebug - React-utils - ReactCodegen @@ -2867,9 +3279,12 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-ImageManager + - React-jsi - React-NativeModulesApple - React-RCTFabric + - React-renderercss - React-rendererdebug - React-utils - ReactCodegen @@ -2888,9 +3303,12 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-ImageManager + - React-jsi - React-NativeModulesApple - React-RCTFabric + - React-renderercss - React-rendererdebug - React-utils - ReactCodegen @@ -2899,29 +3317,33 @@ PODS: - Yoga - react-native-widgetkit (1.0.9): - React - - React-nativeconfig (0.77.2) - - React-NativeModulesApple (0.77.2): + - React-NativeModulesApple (0.79.5): - glog - hermes-engine - React-callinvoker - React-Core - React-cxxreact + - React-featureflags + - React-hermes - React-jsi - React-jsinspector - React-runtimeexecutor - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - React-perflogger (0.77.2): + - React-oscompat (0.79.5) + - React-perflogger (0.79.5): - DoubleConversion - RCT-Folly (= 2024.11.18.00) - - React-performancetimeline (0.77.2): + - React-performancetimeline (0.79.5): - RCT-Folly (= 2024.11.18.00) - React-cxxreact - React-featureflags + - React-jsinspectortracing + - React-perflogger - React-timing - - React-RCTActionSheet (0.77.2): - - React-Core/RCTActionSheetHeaders (= 0.77.2) - - React-RCTAnimation (0.77.2): + - React-RCTActionSheet (0.79.5): + - React-Core/RCTActionSheetHeaders (= 0.79.5) + - React-RCTAnimation (0.79.5): - RCT-Folly (= 2024.11.18.00) - RCTTypeSafety - React-Core/RCTAnimationHeaders @@ -2929,7 +3351,8 @@ PODS: - React-NativeModulesApple - React-RCTFBReactNativeSpec - ReactCommon - - React-RCTAppDelegate (0.77.2): + - React-RCTAppDelegate (0.79.5): + - hermes-engine - RCT-Folly (= 2024.11.18.00) - RCTRequired - RCTTypeSafety @@ -2941,20 +3364,20 @@ PODS: - React-featureflags - React-graphics - React-hermes - - React-nativeconfig + - React-jsitooling - React-NativeModulesApple - React-RCTFabric - React-RCTFBReactNativeSpec - React-RCTImage - React-RCTNetwork + - React-RCTRuntime - React-rendererdebug - React-RuntimeApple - React-RuntimeCore - - React-RuntimeHermes - React-runtimescheduler - React-utils - ReactCommon - - React-RCTBlob (0.77.2): + - React-RCTBlob (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2968,7 +3391,7 @@ PODS: - React-RCTFBReactNativeSpec - React-RCTNetwork - ReactCommon - - React-RCTFabric (0.77.2): + - React-RCTFabric (0.79.5): - glog - hermes-engine - RCT-Folly/Fabric (= 2024.11.18.00) @@ -2979,29 +3402,33 @@ PODS: - React-FabricImage - React-featureflags - React-graphics + - React-hermes - React-ImageManager - React-jsi - React-jsinspector - - React-nativeconfig + - React-jsinspectortracing - React-performancetimeline + - React-RCTAnimation - React-RCTImage - React-RCTText - React-rendererconsistency + - React-renderercss - React-rendererdebug - React-runtimescheduler - React-utils - Yoga - - React-RCTFBReactNativeSpec (0.77.2): + - React-RCTFBReactNativeSpec (0.79.5): - hermes-engine - RCT-Folly - RCTRequired - RCTTypeSafety - React-Core + - React-hermes - React-jsi - React-jsiexecutor - React-NativeModulesApple - ReactCommon - - React-RCTImage (0.77.2): + - React-RCTImage (0.79.5): - RCT-Folly (= 2024.11.18.00) - RCTTypeSafety - React-Core/RCTImageHeaders @@ -3010,14 +3437,14 @@ PODS: - React-RCTFBReactNativeSpec - React-RCTNetwork - ReactCommon - - React-RCTLinking (0.77.2): - - React-Core/RCTLinkingHeaders (= 0.77.2) - - React-jsi (= 0.77.2) + - React-RCTLinking (0.79.5): + - React-Core/RCTLinkingHeaders (= 0.79.5) + - React-jsi (= 0.79.5) - React-NativeModulesApple - React-RCTFBReactNativeSpec - ReactCommon - - ReactCommon/turbomodule/core (= 0.77.2) - - React-RCTNetwork (0.77.2): + - ReactCommon/turbomodule/core (= 0.79.5) + - React-RCTNetwork (0.79.5): - RCT-Folly (= 2024.11.18.00) - RCTTypeSafety - React-Core/RCTNetworkHeaders @@ -3025,7 +3452,20 @@ PODS: - React-NativeModulesApple - React-RCTFBReactNativeSpec - ReactCommon - - React-RCTSettings (0.77.2): + - React-RCTRuntime (0.79.5): + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - React-Core + - React-hermes + - React-jsi + - React-jsinspector + - React-jsinspectortracing + - React-jsitooling + - React-RuntimeApple + - React-RuntimeCore + - React-RuntimeHermes + - React-RCTSettings (0.79.5): - RCT-Folly (= 2024.11.18.00) - RCTTypeSafety - React-Core/RCTSettingsHeaders @@ -3033,25 +3473,28 @@ PODS: - React-NativeModulesApple - React-RCTFBReactNativeSpec - ReactCommon - - React-RCTText (0.77.2): - - React-Core/RCTTextHeaders (= 0.77.2) + - React-RCTText (0.79.5): + - React-Core/RCTTextHeaders (= 0.79.5) - Yoga - - React-RCTVibration (0.77.2): + - React-RCTVibration (0.79.5): - RCT-Folly (= 2024.11.18.00) - React-Core/RCTVibrationHeaders - React-jsi - React-NativeModulesApple - React-RCTFBReactNativeSpec - ReactCommon - - React-rendererconsistency (0.77.2) - - React-rendererdebug (0.77.2): + - React-rendererconsistency (0.79.5) + - React-renderercss (0.79.5): + - React-debug + - React-utils + - React-rendererdebug (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) - RCT-Folly (= 2024.11.18.00) - React-debug - - React-rncore (0.77.2) - - React-RuntimeApple (0.77.2): + - React-rncore (0.79.5) + - React-RuntimeApple (0.79.5): - hermes-engine - RCT-Folly/Fabric (= 2024.11.18.00) - React-callinvoker @@ -3063,6 +3506,7 @@ PODS: - React-jsi - React-jsiexecutor - React-jsinspector + - React-jsitooling - React-Mapbuffer - React-NativeModulesApple - React-RCTFabric @@ -3072,35 +3516,38 @@ PODS: - React-RuntimeHermes - React-runtimescheduler - React-utils - - React-RuntimeCore (0.77.2): + - React-RuntimeCore (0.79.5): - glog - hermes-engine - RCT-Folly/Fabric (= 2024.11.18.00) - React-cxxreact - React-Fabric - React-featureflags + - React-hermes - React-jserrorhandler - React-jsi - React-jsiexecutor - React-jsinspector + - React-jsitooling - React-performancetimeline - React-runtimeexecutor - React-runtimescheduler - React-utils - - React-runtimeexecutor (0.77.2): - - React-jsi (= 0.77.2) - - React-RuntimeHermes (0.77.2): + - React-runtimeexecutor (0.79.5): + - React-jsi (= 0.79.5) + - React-RuntimeHermes (0.79.5): - hermes-engine - RCT-Folly/Fabric (= 2024.11.18.00) - React-featureflags - React-hermes - React-jsi - React-jsinspector + - React-jsinspectortracing + - React-jsitooling - React-jsitracing - - React-nativeconfig - React-RuntimeCore - React-utils - - React-runtimescheduler (0.77.2): + - React-runtimescheduler (0.79.5): - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) @@ -3108,23 +3555,26 @@ PODS: - React-cxxreact - React-debug - React-featureflags + - React-hermes - React-jsi + - React-jsinspectortracing - React-performancetimeline - React-rendererconsistency - React-rendererdebug - React-runtimeexecutor - React-timing - React-utils - - React-timing (0.77.2) - - React-utils (0.77.2): + - React-timing (0.79.5) + - React-utils (0.79.5): - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) - React-debug - - React-jsi (= 0.77.2) - - ReactAppDependencyProvider (0.77.2): + - React-hermes + - React-jsi (= 0.79.5) + - ReactAppDependencyProvider (0.79.5): - ReactCodegen - - ReactCodegen (0.77.2): + - ReactCodegen (0.79.5): - DoubleConversion - glog - hermes-engine @@ -3137,6 +3587,7 @@ PODS: - React-FabricImage - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-NativeModulesApple @@ -3145,55 +3596,55 @@ PODS: - React-utils - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactCommon (0.77.2): - - ReactCommon/turbomodule (= 0.77.2) - - ReactCommon/turbomodule (0.77.2): + - ReactCommon (0.79.5): + - ReactCommon/turbomodule (= 0.79.5) + - ReactCommon/turbomodule (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) - - React-callinvoker (= 0.77.2) - - React-cxxreact (= 0.77.2) - - React-jsi (= 0.77.2) - - React-logger (= 0.77.2) - - React-perflogger (= 0.77.2) - - ReactCommon/turbomodule/bridging (= 0.77.2) - - ReactCommon/turbomodule/core (= 0.77.2) - - ReactCommon/turbomodule/bridging (0.77.2): + - React-callinvoker (= 0.79.5) + - React-cxxreact (= 0.79.5) + - React-jsi (= 0.79.5) + - React-logger (= 0.79.5) + - React-perflogger (= 0.79.5) + - ReactCommon/turbomodule/bridging (= 0.79.5) + - ReactCommon/turbomodule/core (= 0.79.5) + - ReactCommon/turbomodule/bridging (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) - - React-callinvoker (= 0.77.2) - - React-cxxreact (= 0.77.2) - - React-jsi (= 0.77.2) - - React-logger (= 0.77.2) - - React-perflogger (= 0.77.2) - - ReactCommon/turbomodule/core (0.77.2): + - React-callinvoker (= 0.79.5) + - React-cxxreact (= 0.79.5) + - React-jsi (= 0.79.5) + - React-logger (= 0.79.5) + - React-perflogger (= 0.79.5) + - ReactCommon/turbomodule/core (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) - - React-callinvoker (= 0.77.2) - - React-cxxreact (= 0.77.2) - - React-debug (= 0.77.2) - - React-featureflags (= 0.77.2) - - React-jsi (= 0.77.2) - - React-logger (= 0.77.2) - - React-perflogger (= 0.77.2) - - React-utils (= 0.77.2) + - React-callinvoker (= 0.79.5) + - React-cxxreact (= 0.79.5) + - React-debug (= 0.79.5) + - React-featureflags (= 0.79.5) + - React-jsi (= 0.79.5) + - React-logger (= 0.79.5) + - React-perflogger (= 0.79.5) + - React-utils (= 0.79.5) - ReactNativePerformance (4.1.2): - React-Core - RecaptchaInterop (100.0.0) - - RNBootSplash (6.3.1): + - RNBootSplash (6.3.10): - React-Core - - RNCAsyncStorage (1.23.1): + - RNCAsyncStorage (2.1.2): - React-Core - RNCMaskedView (0.3.2): - DoubleConversion @@ -3207,16 +3658,19 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-ImageManager + - React-jsi - React-NativeModulesApple - React-RCTFabric + - React-renderercss - React-rendererdebug - React-utils - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNDateTimePicker (8.2.0): + - RNDateTimePicker (8.4.1): - React-Core - RNDeviceInfo (10.11.0): - React-Core @@ -3235,7 +3689,7 @@ PODS: - Firebase/Firestore (= 11.2.0) - React-Core - RNFBApp - - RNFlashList (1.7.3): + - RNFlashList (1.7.6): - DoubleConversion - glog - hermes-engine @@ -3247,16 +3701,19 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-ImageManager + - React-jsi - React-NativeModulesApple - React-RCTFabric + - React-renderercss - React-rendererdebug - React-utils - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNGestureHandler (2.22.1): + - RNGestureHandler (2.24.0): - DoubleConversion - glog - hermes-engine @@ -3268,9 +3725,12 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-ImageManager + - React-jsi - React-NativeModulesApple - React-RCTFabric + - React-renderercss - React-rendererdebug - React-utils - ReactCodegen @@ -3286,7 +3746,7 @@ PODS: - RNQrGenerator (1.4.3): - React - ZXingObjC - - RNReanimated (3.16.7): + - RNReanimated (3.19.3): - DoubleConversion - glog - hermes-engine @@ -3298,18 +3758,21 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-ImageManager + - React-jsi - React-NativeModulesApple - React-RCTFabric + - React-renderercss - React-rendererdebug - React-utils - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNReanimated/reanimated (= 3.16.7) - - RNReanimated/worklets (= 3.16.7) + - RNReanimated/reanimated (= 3.19.3) + - RNReanimated/worklets (= 3.19.3) - Yoga - - RNReanimated/reanimated (3.16.7): + - RNReanimated/reanimated (3.19.3): - DoubleConversion - glog - hermes-engine @@ -3321,17 +3784,20 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-ImageManager + - React-jsi - React-NativeModulesApple - React-RCTFabric + - React-renderercss - React-rendererdebug - React-utils - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNReanimated/reanimated/apple (= 3.16.7) + - RNReanimated/reanimated/apple (= 3.19.3) - Yoga - - RNReanimated/reanimated/apple (3.16.7): + - RNReanimated/reanimated/apple (3.19.3): - DoubleConversion - glog - hermes-engine @@ -3343,16 +3809,19 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-ImageManager + - React-jsi - React-NativeModulesApple - React-RCTFabric + - React-renderercss - React-rendererdebug - React-utils - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNReanimated/worklets (3.16.7): + - RNReanimated/worklets (3.19.3): - DoubleConversion - glog - hermes-engine @@ -3364,16 +3833,20 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-ImageManager + - React-jsi - React-NativeModulesApple - React-RCTFabric + - React-renderercss - React-rendererdebug - React-utils - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core + - RNReanimated/worklets/apple (= 3.19.3) - Yoga - - RNScreens (4.11.0): + - RNReanimated/worklets/apple (3.19.3): - DoubleConversion - glog - hermes-engine @@ -3385,21 +3858,48 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - RNScreens (4.11.1): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi - React-NativeModulesApple - React-RCTFabric - React-RCTImage + - React-renderercss - React-rendererdebug - React-utils - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNSVG (15.11.2): + - RNSVG (15.13.0): - React-Core - - SDWebImage (5.21.1): - - SDWebImage/Core (= 5.21.1) - - SDWebImage/Core (5.21.1) + - SDWebImage (5.21.3): + - SDWebImage/Core (= 5.21.3) + - SDWebImage/Core (5.21.3) - SDWebImageWebPCoder (0.14.6): - libwebp (~> 1.0) - SDWebImage/Core (~> 5.17) @@ -3425,7 +3925,14 @@ DEPENDENCIES: - "DatadogSDKReactNative (from `../../../node_modules/@datadog/mobile-react-native`)" - DoubleConversion (from `../../../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - "EthersRS (from `../../../node_modules/@uniswap/ethers-rs-mobile`)" + - EXConstants (from `../../../node_modules/expo-constants/ios`) + - EXJSONUtils (from `../../../node_modules/expo-json-utils/ios`) + - EXManifests (from `../../../node_modules/expo-manifests/ios`) - Expo (from `../../../node_modules/expo`) + - expo-dev-client (from `../../../node_modules/expo-dev-client/ios`) + - expo-dev-launcher (from `../../../node_modules/expo-dev-launcher`) + - expo-dev-menu (from `../../../node_modules/expo-dev-menu`) + - expo-dev-menu-interface (from `../../../node_modules/expo-dev-menu-interface/ios`) - ExpoAsset (from `../../../node_modules/expo-asset/ios`) - ExpoBlur (from `../../../node_modules/expo-blur/ios`) - ExpoCamera (from `../../../node_modules/expo-camera/ios`) @@ -3443,6 +3950,7 @@ DEPENDENCIES: - ExpoSecureStore (from `../../../node_modules/expo-secure-store/ios`) - ExpoStoreReview (from `../../../node_modules/expo-store-review/ios`) - ExpoWebBrowser (from `../../../node_modules/expo-web-browser/ios`) + - EXUpdatesInterface (from `../../../node_modules/expo-updates-interface/ios`) - fast_float (from `../../../node_modules/react-native/third-party-podspecs/fast_float.podspec`) - FBLazyVector (from `../../../node_modules/react-native/Libraries/FBLazyVector`) - fmt (from `../../../node_modules/react-native/third-party-podspecs/fmt.podspec`) @@ -3476,6 +3984,8 @@ DEPENDENCIES: - React-jsi (from `../../../node_modules/react-native/ReactCommon/jsi`) - React-jsiexecutor (from `../../../node_modules/react-native/ReactCommon/jsiexecutor`) - React-jsinspector (from `../../../node_modules/react-native/ReactCommon/jsinspector-modern`) + - React-jsinspectortracing (from `../../../node_modules/react-native/ReactCommon/jsinspector-modern/tracing`) + - React-jsitooling (from `../../../node_modules/react-native/ReactCommon/jsitooling`) - React-jsitracing (from `../../../node_modules/react-native/ReactCommon/hermes/executor/`) - React-logger (from `../../../node_modules/react-native/ReactCommon/logger`) - React-Mapbuffer (from `../../../node_modules/react-native/ReactCommon`) @@ -3498,8 +4008,8 @@ DEPENDENCIES: - react-native-video (from `../../../node_modules/react-native-video`) - react-native-webview (from `../../../node_modules/react-native-webview`) - react-native-widgetkit (from `../../../node_modules/react-native-widgetkit`) - - React-nativeconfig (from `../../../node_modules/react-native/ReactCommon`) - React-NativeModulesApple (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`) + - React-oscompat (from `../../../node_modules/react-native/ReactCommon/oscompat`) - React-perflogger (from `../../../node_modules/react-native/ReactCommon/reactperflogger`) - React-performancetimeline (from `../../../node_modules/react-native/ReactCommon/react/performance/timeline`) - React-RCTActionSheet (from `../../../node_modules/react-native/Libraries/ActionSheetIOS`) @@ -3511,10 +4021,12 @@ DEPENDENCIES: - React-RCTImage (from `../../../node_modules/react-native/Libraries/Image`) - React-RCTLinking (from `../../../node_modules/react-native/Libraries/LinkingIOS`) - React-RCTNetwork (from `../../../node_modules/react-native/Libraries/Network`) + - React-RCTRuntime (from `../../../node_modules/react-native/React/Runtime`) - React-RCTSettings (from `../../../node_modules/react-native/Libraries/Settings`) - React-RCTText (from `../../../node_modules/react-native/Libraries/Text`) - React-RCTVibration (from `../../../node_modules/react-native/Libraries/Vibration`) - React-rendererconsistency (from `../../../node_modules/react-native/ReactCommon/react/renderer/consistency`) + - React-renderercss (from `../../../node_modules/react-native/ReactCommon/react/renderer/css`) - React-rendererdebug (from `../../../node_modules/react-native/ReactCommon/react/renderer/debug`) - React-rncore (from `../../../node_modules/react-native/ReactCommon`) - React-RuntimeApple (from `../../../node_modules/react-native/ReactCommon/react/runtime/platform/ios`) @@ -3604,8 +4116,22 @@ EXTERNAL SOURCES: :podspec: "../../../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" EthersRS: :path: "../../../node_modules/@uniswap/ethers-rs-mobile" + EXConstants: + :path: "../../../node_modules/expo-constants/ios" + EXJSONUtils: + :path: "../../../node_modules/expo-json-utils/ios" + EXManifests: + :path: "../../../node_modules/expo-manifests/ios" Expo: :path: "../../../node_modules/expo" + expo-dev-client: + :path: "../../../node_modules/expo-dev-client/ios" + expo-dev-launcher: + :path: "../../../node_modules/expo-dev-launcher" + expo-dev-menu: + :path: "../../../node_modules/expo-dev-menu" + expo-dev-menu-interface: + :path: "../../../node_modules/expo-dev-menu-interface/ios" ExpoAsset: :path: "../../../node_modules/expo-asset/ios" ExpoBlur: @@ -3640,6 +4166,8 @@ EXTERNAL SOURCES: :path: "../../../node_modules/expo-store-review/ios" ExpoWebBrowser: :path: "../../../node_modules/expo-web-browser/ios" + EXUpdatesInterface: + :path: "../../../node_modules/expo-updates-interface/ios" fast_float: :podspec: "../../../node_modules/react-native/third-party-podspecs/fast_float.podspec" FBLazyVector: @@ -3650,7 +4178,7 @@ EXTERNAL SOURCES: :podspec: "../../../node_modules/react-native/third-party-podspecs/glog.podspec" hermes-engine: :podspec: "../../../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec" - :tag: hermes-2024-11-25-RNv0.77.0-d4f25d534ab744866448b36ca3bf3d97c08e638c + :tag: hermes-2025-06-04-RNv0.79.3-7f9a871eefeb2c3852365ee80f0b6733ec12ac3b RCT-Folly: :podspec: "../../../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec" RCTDeprecation: @@ -3701,6 +4229,10 @@ EXTERNAL SOURCES: :path: "../../../node_modules/react-native/ReactCommon/jsiexecutor" React-jsinspector: :path: "../../../node_modules/react-native/ReactCommon/jsinspector-modern" + React-jsinspectortracing: + :path: "../../../node_modules/react-native/ReactCommon/jsinspector-modern/tracing" + React-jsitooling: + :path: "../../../node_modules/react-native/ReactCommon/jsitooling" React-jsitracing: :path: "../../../node_modules/react-native/ReactCommon/hermes/executor/" React-logger: @@ -3745,10 +4277,10 @@ EXTERNAL SOURCES: :path: "../../../node_modules/react-native-webview" react-native-widgetkit: :path: "../../../node_modules/react-native-widgetkit" - React-nativeconfig: - :path: "../../../node_modules/react-native/ReactCommon" React-NativeModulesApple: :path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios" + React-oscompat: + :path: "../../../node_modules/react-native/ReactCommon/oscompat" React-perflogger: :path: "../../../node_modules/react-native/ReactCommon/reactperflogger" React-performancetimeline: @@ -3771,6 +4303,8 @@ EXTERNAL SOURCES: :path: "../../../node_modules/react-native/Libraries/LinkingIOS" React-RCTNetwork: :path: "../../../node_modules/react-native/Libraries/Network" + React-RCTRuntime: + :path: "../../../node_modules/react-native/React/Runtime" React-RCTSettings: :path: "../../../node_modules/react-native/Libraries/Settings" React-RCTText: @@ -3779,6 +4313,8 @@ EXTERNAL SOURCES: :path: "../../../node_modules/react-native/Libraries/Vibration" React-rendererconsistency: :path: "../../../node_modules/react-native/ReactCommon/react/renderer/consistency" + React-renderercss: + :path: "../../../node_modules/react-native/ReactCommon/react/renderer/css" React-rendererdebug: :path: "../../../node_modules/react-native/ReactCommon/react/renderer/debug" React-rncore: @@ -3854,165 +4390,177 @@ SPEC CHECKSUMS: Argon2Swift: 99482c1b8122a03524b61e41c4903a9548e7c33b boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 BoringSSL-GRPC: ca6a8e5d04812fce8ffd6437810c2d46f925eaeb - DatadogCore: 68aee4ffcc3ea17a3b0aa527907757883fc72c84 - DatadogCrashReporting: e6a83b143394e28c9c1cb48c5cfb18eff507b3be - DatadogInternal: 3c5cae6772295fd175a9de11e4747a9322aaa4e7 - DatadogLogs: 09d6358dc7682f9d3eaea85dd418f82d2db3560c - DatadogRUM: 0f267df8c9c8579a291870c2bce4549587391a07 - DatadogSDKReactNative: 55c5868f9321a483bb6f592c1b2948345137a394 - DatadogTrace: f46c8220c73463d09741013f385a6e27cd39185b - DatadogWebViewTracking: dc8376420c8686efd09d00752bc1034b639d180b + DatadogCore: 5c01290a3b60b27bf49aa958f2e339c738364d9e + DatadogCrashReporting: 11286d48ab61baeb2b41b945c7c0d4ef23db317d + DatadogInternal: 7aeb48e254178a0c462c3953dc0a8a8d64499a93 + DatadogLogs: 4324739de62a6059e07d70bf6ceceed78764edeb + DatadogRUM: f36949a38285f3b240a7be577d425f8518e087d4 + DatadogSDKReactNative: 58d9a3f2005f0f9b47c057929c021fcb3b5201e4 + DatadogTrace: bfea32b6ed2870829629a9296cf526221493cc3e + DatadogWebViewTracking: 78c20d8e5f1ade506f4aadaec5690c1a63283fe2 DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb EthersRS: 56b70e73d22d4e894b7e762eef1129159bcd3135 - Expo: 3e53243e3281214a7d613f8a875c0b732d7512c2 - ExpoAsset: 0687fe05f5d051c4a34dd1f9440bd00858413cfe - ExpoBlur: 567af66164e3043a9a30069594aed1ddf0a88d97 - ExpoCamera: 173e000631122854b87c20310513981e89030bc6 - ExpoClipboard: 5250b207b6d545f4e9aac5ea3c6e61c4f16d0aed - ExpoFileSystem: c8c19bf80d914c83dda3beb8569d7fb603be0970 - ExpoFont: 773955186469acc5108ff569712a2d243857475f + EXConstants: 9d62a46a36eae6d28cb978efcbc68aef354d1704 + EXJSONUtils: 1d3e4590438c3ee593684186007028a14b3686cd + EXManifests: f4cc4a62ee4f1c8a9cf2bb79d325eac6cb9f5684 + Expo: 2a8d20c4498052d30c3b198e98cf8c19a137ecd7 + expo-dev-client: f1b99dfea0c9174d2e4ec96c2c5461587dda1e86 + expo-dev-launcher: 73e0cc1a270486501011fd8bed4cb096cc431a43 + expo-dev-menu: b2554d3971b251b2c1f0f5c9c3da50855150f195 + expo-dev-menu-interface: 609c35ae8b97479cdd4c9e23c8cf6adc44beea0e + ExpoAsset: 7bdbbacf4e6752ae6e3cf70555cee076f6229e6e + ExpoBlur: 846780b2c90f59e964b9a50385d4deb67174ebfb + ExpoCamera: fc1ab0e1c665b543a307c577df107e37cc2edc8e + ExpoClipboard: 6b9aae54fd48a579473fb101051ad693435b9294 + ExpoFileSystem: 9681caebda23fa1b38a12a9c68b2bade7072ce20 + ExpoFont: 091a47eeaa1b30b0b760aa1d0a2e7814e8bf6fe6 ExpoHaptics: e01cce0741d68c281853118eb0267f88d42c6b7a - ExpoKeepAwake: 2a5f15dd4964cba8002c9a36676319a3394c85c7 - ExpoLinearGradient: ee9efc5acb988b911320e964fab9b4cbdeb198c4 - ExpoLinking: 0381341519ca7180a3a057d20edb1cf6a908aaf4 - ExpoLocalAuthentication: 64bf2cbee456f5639d69a853684c285afc0602d8 - ExpoLocalization: e36b911e04d371c6c6624ef818e56229bf51c498 - ExpoModulesCore: 87f0b8b38f9d4c8a983212ba54119f11f3fcb615 - ExpoScreenCapture: 29ab5480e0d2b7849691d17f00a70b279cbe6a65 + ExpoKeepAwake: e8dedc115d9f6f24b153ccd2d1d8efcdfd68a527 + ExpoLinearGradient: ce334cff9859da4635c1d8eff6e291b11b04ccbb + ExpoLinking: 343a89ea864a851831fd4495e8aea01cf0f6a36f + ExpoLocalAuthentication: 78f74d187ee51126e1a789d73fee32d6d7e60f1f + ExpoLocalization: 677e45c2536bf918119962f78d7ffeeea317e07d + ExpoModulesCore: 8030601b6028c50a3adf8864dabf43c84c913f43 + ExpoScreenCapture: 329c26be22741077b81612de1edaee8648fb209e ExpoSecureStore: d006eea5e316283099d46f80a6b10055b89a6008 - ExpoStoreReview: 32f7186925fdecacddf3c1bc9628dd11b10c3ddd - ExpoWebBrowser: 6890a769e6c9d83da938dceb9a03e764afc3ec9c + ExpoStoreReview: bed43bea90a5876a6a480504f95fea1521dacaa7 + ExpoWebBrowser: eeb47f52e85b2686b56178749675cf90d0822f86 + EXUpdatesInterface: 64f35449b8ef89ce08cdd8952a4d119b5de6821d fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 - FBLazyVector: 4c16dde959a9d6b24f2aa32cb87cb919a1ace3f3 + FBLazyVector: d2a9cd223302b6c9aa4aa34c1a775e9db609eb52 Firebase: 98e6bf5278170668a7983e12971a66b2cd57fc8c - FirebaseAppCheckInterop: a92ba81d0ee3c4cddb1a2e52c668ea51dc63c3ae + FirebaseAppCheckInterop: 06fe5a3799278ae4667e6c432edd86b1030fa3df FirebaseAuth: 2a198b8cdbbbd457f08d74df7040feb0a0e7777a - FirebaseAuthInterop: e25b58ecb90f3285085fa2118861a3c9dfdc62ad + FirebaseAuthInterop: 7087d7a4ee4bc4de019b2d0c240974ed5d89e2fd FirebaseCore: a282032ae9295c795714ded2ec9c522fc237f8da FirebaseCoreExtension: f1bc67a4702931a7caa097d8e4ac0a1b0d16720e - FirebaseCoreInternal: 6a3b668197644aa858fc4127578637c6767ba123 + FirebaseCoreInternal: 9afa45b1159304c963da48addb78275ef701c6b4 FirebaseFirestore: 62708adbc1dfcd6d165a7c0a202067b441912dc9 FirebaseFirestoreInternal: ad9b9ee2d3d430c8f31333a69b3b6737a7206232 - FirebaseSharedSwift: bdd5c8674c4712a98e70287c936bc5cca5d640f6 + FirebaseSharedSwift: e17c654ef1f1a616b0b33054e663ad1035c8fd40 fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd - glog: eb93e2f488219332457c3c4eafd2738ddc7e80b8 + glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 "gRPC-C++": 2fa52b3141e7789a28a737f251e0c45b4cb20a87 gRPC-Core: a27c294d6149e1c39a7d173527119cfbc3375ce4 GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 - hermes-engine: 8eb265241fa1d7095d3a40d51fd90f7dce68217c + hermes-engine: f03b0e06d3882d71e67e45b073bb827da1a21aae leveldb-library: cc8b8f8e013647a295ad3f8cd2ddf49a6f19be19 libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 - MMKV: b4802ebd5a7c68fc0c4a5ccb4926fbdfb62d68e0 - MMKVCore: a255341a3746955f50da2ad9121b18cb2b346e61 + MMKV: 1a8e7dbce7f9cad02c52e1b1091d07bd843aefaf + MMKVCore: f2dd4c9befea04277a55e84e7812f930537993df nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 OneSignalXCFramework: 1a3b28dfbff23aabce585796d23c1bef37772774 OpenTelemetrySwiftApi: aaee576ed961e0c348af78df58b61300e95bd104 PLCrashReporter: db59ef96fa3d25f3650040d02ec2798cffee75f2 RCT-Folly: 36fe2295e44b10d831836cc0d1daec5f8abcf809 - RCTDeprecation: 85b72250b63cfb54f29ca96ceb108cb9ef3c2079 - RCTRequired: 567cb8f5d42b990331bfd93faad1d8999b1c1736 - RCTTypeSafety: 5e57924492a5e0a762654f814dd018953274eca9 - React: 53c9bd6f974c5dd019ee466e46477eb679149c38 - React-callinvoker: d6484472c1c742917b51338525336d6a74ab8a9f - React-Core: 043aaf319142ecc02db6fffccb780186e6e7462a - React-CoreModules: abe0b2089368e420b7beaa5e140771181e2f6edb - React-cxxreact: 8b678dd36228089b6ee19d62f2bfb8935ea1c63b - React-debug: af25f71a2ea800559f591ef9e9e6495a206c4f7c - React-defaultsnativemodule: 7883e6ef963ee6a6346eb43b73967919029f1033 - React-domnativemodule: eafabfac38103187bfd0bc8b4c64d1ebc35444c2 - React-Fabric: 72038b554a0e4791432a7c161e6ea52bdc854d7c - React-FabricComponents: 47e25c62a3fdc4d6630dc44f41c5552a80202a2a - React-FabricImage: 95c3a49e22cc8c5c33cf282287c73cf261edd104 - React-featureflags: 05545ed41078babfec20095fd7825f029709cde6 - React-featureflagsnativemodule: 75f805793e3feb0dea7055e92540624965543de9 - React-graphics: e33b1bff03c62a7293991bcc28ceb946740bae0f - React-hermes: c07c778ab9cb80a943116ef4574087e8570206cf - React-idlecallbacksnativemodule: f800ae00e3427cbf0ae1f7d880ccbbe8ffea5e16 - React-ImageManager: ca548168b2efedd1e017dc6d715f5e0028eb446a - React-jserrorhandler: 8a7c11b5691b798c9b25b8e4cfbda02742828602 - React-jsi: 1c901c8f6e8d4555b6a5747c5a5b7c7cf757da71 - React-jsiexecutor: d59faf2904bb0bd1daec18bc59731623e79a74eb - React-jsinspector: eb7486a37a90aa2e896d7c67d7ea04c1c466b4ef - React-jsitracing: a417d71b554e891ccab72510f9482e6c53d0b09a - React-logger: 5320a2acb25baa566cda59b611231929dd3ed7fc - React-Mapbuffer: 9af3695e354816d30d4429992714e0c8cefac25f - React-microtasksnativemodule: ab4c1c9d4841e71a4116f167a4cd3f8dc1ab50ba + RCTDeprecation: 5f638f65935e273753b1f31a365db6a8d6dc53b5 + RCTRequired: 8b46a520ea9071e2bc47d474aa9ca31b4a935bd8 + RCTTypeSafety: cc4740278c2a52cbf740592b0a0a40df1587c9ab + React: 6393ae1807614f017a84805bf2417e3497f518a6 + React-callinvoker: c34f666f551f05a325b87e7e3e6df0e082fa3d99 + React-Core: fc07a4b69a963880b25142c51178f4cb75628c7d + React-CoreModules: 94d39315cfa791f6c477712fea47c34f8ecb26c6 + React-cxxreact: 628c28cdb3fdef93ee3bfc2bec8e2d776e81ae49 + React-debug: c1b10e5982b961738eab5b1d66fa31572ca28b5e + React-defaultsnativemodule: dd13932a4a4b0f1d556c9a4e76cc00fa05207126 + React-domnativemodule: 44bd8074cffa6a8ed298a45e2f7d1b519fb15a23 + React-Fabric: 5efe9e2171352089ded33d73e177345e8eb74e00 + React-FabricComponents: 359ea9205fc116ec52c90186ace048391ba477f7 + React-FabricImage: e536ff5e1c1f081dc5953696287e33d4d3b543bd + React-featureflags: 1e3a098a98c63a339a8b5ef4014ba4c4b43fb1f6 + React-featureflagsnativemodule: c926c24fda31daab491e25f4003413b49a5dfaec + React-graphics: 1476634e2deaf13dad3ab7ddfa33f78933445e07 + React-hermes: af1b3d79491295abc9d1b11f84e77d5dc00095b6 + React-idlecallbacksnativemodule: 43fc456e78c7dd7a342a9f185ef7c931d8c44ab0 + React-ImageManager: bd97427edf2df85e7e162e2161c9c981a6185915 + React-jserrorhandler: 44ebfb576a9ce098205b246a4bb81c9ad55ffbb6 + React-jsi: e9c3019e00db5d144e0a660616a52a605e12c39a + React-jsiexecutor: 3ed70a394b76f33e6c4ec4b382a457df7309d96c + React-jsinspector: d0c7ef76573b8e2b362ebc570aecd43b0c88a282 + React-jsinspectortracing: 551b7981d2a0b6a7829fd8c8c310ca51b5b323f8 + React-jsitooling: 5d06fc7c61ac1d260a553a9a9cffcd78865e430c + React-jsitracing: 2ecfa3ccc58e876a8c4f76a2cbdd920fc1ccfbb0 + React-logger: e6e6164f1753e46d1b7e2c8f0949cd7937eaf31b + React-Mapbuffer: a83853bd80bb31a4451e64af91a86c02f7df4f80 + React-microtasksnativemodule: 964a2c1213bb39fa5cc8d5ee4f55846d67747f32 react-native-appsflyer: 2cc1f96348065fc23e976fc7a27e371789fb349e - react-native-compat: 17cc6a63937e3fc291b92250f56ce5e9c0c3aa53 + react-native-compat: 1e09d1a14355b6a0383fb371fe98509fa8c3f4fd react-native-context-menu-view: dcec18eb8882e20596dbb75802e7d19cb87dac02 react-native-get-random-values: 21325b2244dfa6b58878f51f9aa42821e7ba3d06 - react-native-image-picker: 2064a7a43d1e204c3b42ce6d2208df32e881fc9d - react-native-keyboard-controller: 2fdaf70d94da51a982c702720fdc7b051db8140b + react-native-image-picker: 75cc6db21e264e573456725c71ad21828a82c455 + react-native-keyboard-controller: 8698f5ff79ab0a4a8f0259a40b4c3c7f23cae7fd react-native-mmkv: dea675cf9697ad35940f1687e98e133e1358ef9f react-native-netinfo: f0a9899081c185db1de5bb2fdc1c88c202a059ac react-native-onesignal: 33ade92bd91578374c31c5a5a91f45f49c2d6614 - react-native-pager-view: b8b7c09ce10ed0ca632689570aa1020271b44156 + react-native-pager-view: d6f91626b36fcca51d28a9c5ec109a9309242089 react-native-passkey: 69bede03f6bb35fad8117cad73155231cc31066c react-native-restart: 7595693413fe3ca15893702f2c8306c62a708162 - react-native-safe-area-context: 04803a01f39f31cc6605a5531280b477b48f8a88 - react-native-skia: 628fde753bb219b462d97d761f028076d289b1a1 - react-native-slider: bf50824dd00db1e7b66eaec12598883cea3c79e7 - react-native-video: 85e6571bb240a1f084cc7fc9b2f2f0c27b51b622 - react-native-webview: 6c92617eff2519f6359440508c603d3ecda50984 + react-native-safe-area-context: 8870dc3e45c8d241336cd8ee3fa3fc76f3a040ac + react-native-skia: eee8f5b2560445bea34c27bb9f0b2dc7adb04e53 + react-native-slider: d3ddeb61d8c4c4d99f19194338d8d2c33957e717 + react-native-video: a6a2ad5d778133dee45875faf44c6ce0d61cac0e + react-native-webview: 94294e5a5d8cf4f53793aee7ef69ebd14c79e397 react-native-widgetkit: efb6680df237463bbe1be3a4d1a1578a1b0bb08f - React-nativeconfig: 75658bde8f977492f668e94ae8eb9c0dfef7ed94 - React-NativeModulesApple: 1bd9fa09c40204ac489acbe7da239efa4aa47244 - React-perflogger: a0f49e229d1252d683102df60d2392229ece5837 - React-performancetimeline: d0fb47dfc5d55ec6b7c4a79c83912ff59bb8df51 - React-RCTActionSheet: 150cfe1df4275db2251a2a4a1b22be3294e94ef7 - React-RCTAnimation: 67a303df28e0981f58a26968b3c15252a6b277b8 - React-RCTAppDelegate: 5ffb34a2bc043815e470d64feb8510d8930d7e40 - React-RCTBlob: 2ccb60fd765ed41292c495e4374eaf7c23a3c81c - React-RCTFabric: ab786f111eb621bcf1e5b3e6f91eaecaf99eadeb - React-RCTFBReactNativeSpec: e943ce6a5952941d730758fcd5b4a603c6b5ea0a - React-RCTImage: f235db428b4c98cbb1ad6304f826695f19e82c57 - React-RCTLinking: 523a01769de55660743d6332157dfb4dbad817b8 - React-RCTNetwork: d99ee5bf1f15ad8521d30c9da477906d7fb00110 - React-RCTSettings: 4165a44c6f51787980634bd522f3b379ee8531a6 - React-RCTText: 462ab8d4f2f180be83e3983307ce5ef5fa58210e - React-RCTVibration: ca784cb7e30c21852d4b78b1621bfa11940ab061 - React-rendererconsistency: 28f87593201bca785e0bbdc94bff4d92ee2d32ee - React-rendererdebug: c3121b6c4f0873ad2cf1bb63947c0a8a0ae8a1ab - React-rncore: df9c0360d3f28371a103921890e20c309c906407 - React-RuntimeApple: 4b8060742249e0ede1e186e3df656cea119ac9da - React-RuntimeCore: c622d85e3fd6f60ccb7f54bb0d583a85dc1dff25 - React-runtimeexecutor: e6e7af01f9989f931289250ee9060604bc0f0144 - React-RuntimeHermes: 5062f63b39a375c63909c16b33bbc24e2c8e7cf6 - React-runtimescheduler: b6788737ee06eec93c63a2cda4b20c5ca916f5b5 - React-timing: dfac299d2afa69272d469c8e5fd4d4328fe41d1e - React-utils: 280b2ed61cfb45ad94fc8e86d7be402689714962 - ReactAppDependencyProvider: 8955603808eb24bbfde3511d2bcc8362d1d5860e - ReactCodegen: 1e8981b03b6389301f819833f8b3b498d9d4da8d - ReactCommon: ad39e4549e2920b3b065a28603921a75619d0639 + React-NativeModulesApple: d2b9bd7d55dfd864e56c76592777ad32e8ab1f3d + React-oscompat: 0592889a9fcf0eacb205532028e4a364e22907dd + React-perflogger: 634408a9a0f5753faa577dfa81bc009edca01062 + React-performancetimeline: b58a6e65c9fbe1aa02b97152edd6ad5275aef36b + React-RCTActionSheet: ce67bdc050cc1d9ef673c7a93e9799288a183f24 + React-RCTAnimation: 12193c2092a78012c7f77457806dcc822cc40d2c + React-RCTAppDelegate: b0a8aa38e4791915673a7a3ae80b2840a81ec255 + React-RCTBlob: 923cf9b0098b9a641cb1e454c30a444d9d3cda70 + React-RCTFabric: 3f2f2980ad1d426f3a03c9183521b835e0bbbe83 + React-RCTFBReactNativeSpec: b7671d70d65f61326805725b24c7855aab0befb2 + React-RCTImage: 580a5d0a6fdf9b69629d0582e5fb5a173e152099 + React-RCTLinking: 4ed7c5667709099bfd6b2b6246b1dfd79c89f7cb + React-RCTNetwork: 06a22dd0088392694df4fd098634811aa0b3e166 + React-RCTRuntime: 38591d6246389f4f8b93f0f94f565f8448805581 + React-RCTSettings: 9dbf433f302c8ebe43b280453e74624098fbc706 + React-RCTText: 92fcd78d6c44dbe64d147bb63f53698bcba7c971 + React-RCTVibration: 513659394c92491e6c749e981424f6e1e0abdb3c + React-rendererconsistency: c9c28e3b0834d9be2e6aa0ba2d1fd77c76441658 + React-renderercss: 700c57db7fcb36a1e5a9b3645f4cc22f4de43899 + React-rendererdebug: 8ce5f50fd01160e1d1bfc9ec34dac0ca5411afc2 + React-rncore: 289894dda4ebcca06104070f1a9c9283f37dd123 + React-RuntimeApple: 5e5315e698c4fc4e5a3e6b610e4e0ae135f3718c + React-RuntimeCore: 23b6e0e4e2b1cb8a81a14db68814ade8998a5fb0 + React-runtimeexecutor: ebfd71307b3166c73ac0c441c1ea42e0f17f821d + React-RuntimeHermes: baef54b36a6623ea8cd7442027744b08ad06d01b + React-runtimescheduler: 4d9a1afaa16d7dd11a909a9103b18b63995c5683 + React-timing: 0f749e1c5ca1147b699b25ec79003950e6366056 + React-utils: 52ce70a20366bb7f1b416c57281f40ffafeb8a8a + ReactAppDependencyProvider: c42e7abdd2228ae583bdabc3dcd8e5cda6bef944 + ReactCodegen: 0851536ada69d85536f54a7239956a50b6dcd42e + ReactCommon: 3dbed0a44e9e5d7b77b0f35983633aa5d33c9694 ReactNativePerformance: ab7dee4c4862623d72c1530a9fc71b55458edf71 RecaptchaInterop: 7d1a4a01a6b2cb1610a47ef3f85f0c411434cb21 - RNBootSplash: 66c8458007bda40cc25a3f25e4326244a71d9a73 - RNCAsyncStorage: 826b603ae9c0f88b5ac4e956801f755109fa4d5c - RNCMaskedView: 9953b54e4f488389ec45d8befd8687d71e7c121e - RNDateTimePicker: 40ffda97d071a98a10fdca4fa97e3977102ccd14 + RNBootSplash: 2c6b226e3ad3c97d16b6d53bd75d0cd281646bfb + RNCAsyncStorage: addfc2cb6511dbe199c56c6b26ede383b6c38919 + RNCMaskedView: 42f3684c136239957b410dbfa81978b25f2c0e18 + RNDateTimePicker: 279bad2682d9ebdd4151cb71d88f3b460f818fc8 RNDeviceInfo: bf8a32acbcb875f568217285d1793b0e8588c974 RNFastImage: 074e3c1a0d65e2971f28299a85d0155c3b2c948e RNFBApp: 4122dd41d8d7ff017b6ecf777a6224f5b349ca04 RNFBAuth: 1632cefd787a43ba952fa52ff016e7b69fe355cb RNFBFirestore: 5f110e37b7f7f3d6e03c85044dd4cf3ebacec38b - RNFlashList: 4afe189d83616f240be187f717320ad966f6024f - RNGestureHandler: ab4058d59c000e7df387ad9a973e93f7e40de331 + RNFlashList: 7c43eac420e04bfa7798d40c7246c7067e8a4c2c + RNGestureHandler: eb5ad44465a546182d05aebae304e45c881d2f22 RNImageColors: 9ac05083b52d5c350e6972650ae3ba0e556466c1 RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 RNPermissions: 87aac13521bea6dcb6dfd60b03ac69741ccef2b4 RNQrGenerator: ac6a6c766e80dd3625038929ed2b13e2f3edcafb - RNReanimated: 9f96886ec1e1772ed7462cc80da82b7bf1ba984d - RNScreens: 567d3119f7d5d1041090eab25027667c505e4e75 - RNSVG: 4cbae6c6f0ef2b0aa277c5fce8447d3e4cd97cd0 - SDWebImage: f29024626962457f3470184232766516dee8dfea + RNReanimated: ad46062e119fcf93712dfe9dcf72b45ea16892e4 + RNScreens: edd4795b025d94f879e20cc346b844176d938f0c + RNSVG: 204b068da3a7416d22840cd3233bcf745443a455 + SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 sparkfabrik-react-native-idfa-aaid: 1b72a6264a2175473e309ffa6434db87c58af264 UIImageColors: d2ef3b0877d203cbb06489eeb78ea8b7788caabe - Yoga: fdc0542faa3ba87e56f2030b3f3f2e21bc3ba01c + Yoga: bfcce202dba74007f8974ee9c5f903a9a286c445 ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5 -PODFILE CHECKSUM: 5438afe50eaeebf354579fd9c40fb3c93d56ce9b +PODFILE CHECKSUM: 9a816e7213ef1e27594bb84425b559319371f327 -COCOAPODS: 1.14.3 +COCOAPODS: 1.16.2 diff --git a/apps/mobile/ios/Uniswap.xcodeproj/project.pbxproj b/apps/mobile/ios/Uniswap.xcodeproj/project.pbxproj index 6cdc18319f2..3373460591b 100644 --- a/apps/mobile/ios/Uniswap.xcodeproj/project.pbxproj +++ b/apps/mobile/ios/Uniswap.xcodeproj/project.pbxproj @@ -10,13 +10,14 @@ 0013F5F72C93399400D6EF09 /* ProtectionInfo.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0013F5F62C93399400D6EF09 /* ProtectionInfo.graphql.swift */; }; 00265F792C933CE300A5DA57 /* ProtectionResult.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00265F772C933CE300A5DA57 /* ProtectionResult.graphql.swift */; }; 00265F7A2C933CE300A5DA57 /* ProtectionAttackType.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00265F782C933CE300A5DA57 /* ProtectionAttackType.graphql.swift */; }; - 0094F4FBBC1C0A2FFABF7157 /* TokenProject.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4981A14906A35E8A0B7F9457 /* TokenProject.graphql.swift */; }; 00E356F31AD99517003FC87E /* UniswapTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* UniswapTests.m */; }; - 03291E0DA448AF438F97EA5D /* DescriptionTranslations.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E1F288B7EDDE906C4C00C46 /* DescriptionTranslations.graphql.swift */; }; + 02B7B534DFEC9DA0F7F06B8D /* TokenBalanceQuantityParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B0A1F5F6FC3E1141E228B7D /* TokenBalanceQuantityParts.graphql.swift */; }; 037C5AAA2C04970B00B1D808 /* CopyIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037C5AA92C04970B00B1D808 /* CopyIcon.swift */; }; + 03AE019583139330A3E408AB /* SwapOrderDetails.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53F4E5E9341BE66984935185 /* SwapOrderDetails.graphql.swift */; }; 03C788232C10E7390011E5DC /* ActionButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C788222C10E7390011E5DC /* ActionButtons.swift */; }; 03D2F3182C218D390030D987 /* RelativeOffsetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D2F3172C218D380030D987 /* RelativeOffsetView.swift */; }; - 03E3515E38E247F459218CAA /* SwapOrderDetails.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = C955CE2592785712A11892DE /* SwapOrderDetails.graphql.swift */; }; + 03EEEDA1A3D5EF23C0D14B08 /* OnRampTransactionDetails.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87CF9B03AE29A19C5B0F4E35 /* OnRampTransactionDetails.graphql.swift */; }; + 06C9F16E22B8B64855980A69 /* TokenMarketParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55081ADF188C967FC1ECD289 /* TokenMarketParts.graphql.swift */; }; 0703EE032A5734A600AED1DA /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0703EE022A5734A600AED1DA /* UserDefaults.swift */; }; 0703EE052A57351800AED1DA /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 072F6C372A44BECC00DA720A /* Logging.swift */; }; 072E238E2A44D5BD006AD6C9 /* WidgetsCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 072E23862A44D5BC006AD6C9 /* WidgetsCore.framework */; }; @@ -109,6 +110,7 @@ 0743223D2A83E3CA00F8518D /* IAmount.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321E92A83E3C900F8518D /* IAmount.graphql.swift */; }; 0743223E2A83E3CA00F8518D /* IContract.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321EA2A83E3C900F8518D /* IContract.graphql.swift */; }; 074322402A841BBD00F8518D /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0743223F2A841BBD00F8518D /* Constants.swift */; }; + 0764D0BF05C44615BDD9AD65 /* NftBalanceAssetInput.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 696F6C5220D0072E380E198F /* NftBalanceAssetInput.graphql.swift */; }; 0767E0382A65C8330042ADA2 /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0767E0372A65C8330042ADA2 /* Colors.swift */; }; 0767E03B2A65D2550042ADA2 /* Styling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0767E03A2A65D2550042ADA2 /* Styling.swift */; }; 077E60392A85587800ABC4B9 /* TokensQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 077E60382A85587800ABC4B9 /* TokensQuery.graphql.swift */; }; @@ -123,10 +125,10 @@ 07F136422A5763480067004F /* Network.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07F136412A5763480067004F /* Network.swift */; }; 07F5CF712A6AD97D00C648A5 /* Chart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07F5CF702A6AD97D00C648A5 /* Chart.swift */; }; 07F5CF752A7020FD00C648A5 /* Format.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07F5CF742A7020FD00C648A5 /* Format.swift */; }; - 09E8C497051C37FE604FD40E /* OnRampTransactionsAuth.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 315B8A905FA2C60C053F138B /* OnRampTransactionsAuth.graphql.swift */; }; - 09F9DEB33392F3051BEA8D52 /* TokenFeeDataParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 018603D0FA4D05DBF16F1441 /* TokenFeeDataParts.graphql.swift */; }; - 0C6FC49E0FC9BCB2DF5B627E /* TokenSortableField.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32012AC135EBD991C8F02F92 /* TokenSortableField.graphql.swift */; }; - 0CEBEB8BE3AB95C3F584F6CE /* Image.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31ED613D63FE9C56D9270517 /* Image.graphql.swift */; }; + 092E5F0FE7DE6113285AF5E3 /* NftsTabQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3BCD49511DE84892B34ED0 /* NftsTabQuery.graphql.swift */; }; + 0AC6A2E1C5837AAD902742DA /* TokenBalanceMainParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD452B34344D670BA23EA331 /* TokenBalanceMainParts.graphql.swift */; }; + 0AD9B4E176D2E9FE86E6ED21 /* AssetActivity.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05C66C49AF2B78703FCE4C7C /* AssetActivity.graphql.swift */; }; + 0D87DBFD19D5A6DF176C0AB7 /* MobileSchema.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87442A651C068E552BEA89C6 /* MobileSchema.graphql.swift */; }; 0DB282262CDADB260014CF77 /* EmbeddedWallet.m in Sources */ = {isa = PBXBuildFile; fileRef = 0DB282242CDADB260014CF77 /* EmbeddedWallet.m */; }; 0DB282272CDADB260014CF77 /* EmbeddedWallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DB282252CDADB260014CF77 /* EmbeddedWallet.swift */; }; 0DC6ADF02B1E2C100092909C /* PortfolioValueModifier.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DC6ADEF2B1E2C0F0092909C /* PortfolioValueModifier.graphql.swift */; }; @@ -134,61 +136,56 @@ 0DE251472C13B69D005F47F9 /* OnRampTransfer.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DE251442C13B69D005F47F9 /* OnRampTransfer.graphql.swift */; }; 0DE251482C13B69D005F47F9 /* OnRampServiceProvider.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DE251452C13B69D005F47F9 /* OnRampServiceProvider.graphql.swift */; }; 0DE251492C13B69D005F47F9 /* OnRampTransactionDetails.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DE251462C13B69D005F47F9 /* OnRampTransactionDetails.graphql.swift */; }; - 0F282C26344F1AE3622232B0 /* OffRampTransactionDetails.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 396B6AB3BB41898B56EB3828 /* OffRampTransactionDetails.graphql.swift */; }; - 126CC4BC99F3F15CC1E2F1C3 /* NftAssetTrait.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70851E3D1A4B296A1CE22A20 /* NftAssetTrait.graphql.swift */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; - 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; - 143C638C2CCAC689371BFC93 /* NftActivityType.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D95FE36D2667E070345CDD0 /* NftActivityType.graphql.swift */; }; 1440B371A1C9A42F3E91DAAE /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DF5F26A06553EFDD4D99214 /* ExpoModulesProvider.swift */; }; - 171DD1C966C7FBF9DD68CFAC /* TopTokensQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04E5C2900254E7BA7F10CE51 /* TopTokensQuery.graphql.swift */; }; - 1B59968CF49C45B127E9C768 /* NftOrder.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E3A78386B3B779B688021FF /* NftOrder.graphql.swift */; }; - 1BC3A49161EB906542F8E23B /* ProtectionAttackType.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40E034582FBD2C374CEF808E /* ProtectionAttackType.graphql.swift */; }; - 1C3559D557BC09C709104802 /* NftsTabQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 730CA356D6E1D498CC0C1472 /* NftsTabQuery.graphql.swift */; }; - 1CE49305114BF4792290DDC6 /* AmountChange.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E63EA0C75D8011BCC182492C /* AmountChange.graphql.swift */; }; - 1DC34AE3E11264475E8B9F62 /* NftApproval.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E32BBB65A6DD9CFF608C8E8 /* NftApproval.graphql.swift */; }; - 1E2AF2C38C8FBEB2A95B644E /* OffRampTransfer.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9D5E6A07F404974045BD525 /* OffRampTransfer.graphql.swift */; }; - 252BD40CB9B46FE47B2440B1 /* NftCollection.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63ACFF2A1D5AE3498731AC69 /* NftCollection.graphql.swift */; }; - 257C7A9F2C4AC3C99C6B7348 /* TransactionHistoryUpdaterQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF3D7A923E5E1FB504E1F64D /* TransactionHistoryUpdaterQuery.graphql.swift */; }; - 2B12DFE797E2CBF314565D81 /* TokenProtectionInfoParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BB4A40E30E1D985519DB3CF /* TokenProtectionInfoParts.graphql.swift */; }; - 2B422D705C68BB51FBDA9895 /* NFTItemScreenQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 207F4DF7664454C31DE069F8 /* NFTItemScreenQuery.graphql.swift */; }; - 2E05037B51AF526A47701B09 /* TransactionDirection.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC2FBE9F353D3753FB94B8BF /* TransactionDirection.graphql.swift */; }; - 302E24504C4FCE674CF95984 /* TokenParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98F596250F55A3D6E9E1F499 /* TokenParts.graphql.swift */; }; - 30872BFB39EEC66944E3EFD7 /* MobileSchema.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4E0C448C5D45BF1071FA458 /* MobileSchema.graphql.swift */; }; - 31661D70B58410EA030E2C53 /* TimestampedAmount.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0DB67D6C438F623976727E9 /* TimestampedAmount.graphql.swift */; }; - 326CFFDBB89D95FA4CB95EBB /* HistoryDuration.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF42F88002431BFDC9FCA9F /* HistoryDuration.graphql.swift */; }; - 3361FE5837059AEF4AA877BE /* TransactionListQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 224CA82F1871016F4173F69E /* TransactionListQuery.graphql.swift */; }; - 33928A7366AFE7F892CDC89F /* NftBalanceConnection.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FC7B80EC0E0D2134B67575B /* NftBalanceConnection.graphql.swift */; }; - 35B8176433A98BA798BBEE79 /* PageInfo.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E475EBABF7EA310435E2F19C /* PageInfo.graphql.swift */; }; - 36E601F269D40A67FC353947 /* AssetChange.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39F60B2B54796DC978765C99 /* AssetChange.graphql.swift */; }; - 3E338E14E33C721DAB708664 /* NftActivityFilterInput.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43E2ECCEFBC3BE8D13BE6CAB /* NftActivityFilterInput.graphql.swift */; }; - 3FE25F715DFFD1D03DCDA57D /* NftContract.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B9EB43501127BE1B14F1A5 /* NftContract.graphql.swift */; }; + 147F4DFD43CE86324A066003 /* FeedTransactionListQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EFADDBB38BB9261933C1349 /* FeedTransactionListQuery.graphql.swift */; }; + 15193A0A3CE80FF72AAB54B4 /* SafetyLevel.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22F8093091A9E5FC0D6284BA /* SafetyLevel.graphql.swift */; }; + 15A72E27235628C56431EC98 /* FeeData.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEE1CB436B299B0349BA9957 /* FeeData.graphql.swift */; }; + 15F6E43DA2BD9A8A681EC70C /* TokenTransfer.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BEBBC668E69EE3A1FC6AB05 /* TokenTransfer.graphql.swift */; }; + 16506815CDEE17670D3DD363 /* TimestampedAmount.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31CBA4397C44A04E218AEF28 /* TimestampedAmount.graphql.swift */; }; + 19BC2371F6BE6CAD5218AA53 /* NftActivityConnection.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBE0699A65BEEEE6E9A6B03F /* NftActivityConnection.graphql.swift */; }; + 1C84E14C7F52CF6C9A6D2930 /* TokenProjectDescriptionQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1276D33B06E840B8E6839F39 /* TokenProjectDescriptionQuery.graphql.swift */; }; + 1CA252C75CD5291E2A0B308B /* NftCollection.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = B25620D6E0E297C56198F190 /* NftCollection.graphql.swift */; }; + 206E0025B8FCE0EA96AC744A /* NftCollectionScreenQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA3BFDC64C5E0A058877A348 /* NftCollectionScreenQuery.graphql.swift */; }; + 23B195B262495EABE4A6CDF4 /* HistoryDuration.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1053483279043EED7612BE90 /* HistoryDuration.graphql.swift */; }; + 24B4011ADC672F470EC515AC /* BridgedWithdrawalInfo.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAC62CDEEBE5712219A457C7 /* BridgedWithdrawalInfo.graphql.swift */; }; + 252A28057D1D481D14D2F5E5 /* TransactionDirection.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D494D944670E557175A694 /* TransactionDirection.graphql.swift */; }; + 2925C6E0262B95C5EA0C2AE7 /* TransactionDetails.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55240C84512D3851C8B9F027 /* TransactionDetails.graphql.swift */; }; + 2B2738BAB9E906B561E0D4D6 /* TokenParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB76D1207400E308430A837F /* TokenParts.graphql.swift */; }; + 2C9935142A9582467B5B5FC5 /* ProtectionInfo.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD12FD368B3A96F323444D8E /* ProtectionInfo.graphql.swift */; }; + 2D73D2D80BC8C455DB35CE57 /* TokenApproval.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD56CBC543897BE8E37CC2C1 /* TokenApproval.graphql.swift */; }; + 2FC53D1C58218F0BF1D4E5F3 /* IContract.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9189C818AFF8E988F861D0 /* IContract.graphql.swift */; }; + 31A4EC91F1924E25AECA2F4E /* NftCollectionMarket.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = F286C514E849B29F5235310A /* NftCollectionMarket.graphql.swift */; }; + 3389D45727F15B8E4F59B99F /* NftApproval.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD111A711D83D1D82E4025A9 /* NftApproval.graphql.swift */; }; + 34610491FF9A452F4F806157 /* TokenPriceHistoryQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9602E2A290179F8741EDD35A /* TokenPriceHistoryQuery.graphql.swift */; }; + 3720F641F397A2819500B1B1 /* SwapOrderStatus.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0359DDEA2E2E17759FCC5CF /* SwapOrderStatus.graphql.swift */; }; + 391FD815120230BDAF53F6F0 /* BlockaidFees.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EF9AEC00868BFD7CEBF202C /* BlockaidFees.graphql.swift */; }; + 39DEA445993253BCC1201199 /* NftApproveForAll.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1B893C0275DAAA156B0108 /* NftApproveForAll.graphql.swift */; }; + 4260B9F719F8E25DFBF99D73 /* OnRampTransfer.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E5D1A04B42B25218C53C107 /* OnRampTransfer.graphql.swift */; }; + 438115E2759B1194751A8021 /* NftContract.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55A243177E9FA92A0524A222 /* NftContract.graphql.swift */; }; + 45FFF7DF2E8C2A8100362570 /* SilentPushEventEmitter.m in Sources */ = {isa = PBXBuildFile; fileRef = 45FFF7DE2E8C2A6400362570 /* SilentPushEventEmitter.m */; }; + 45FFF7E12E8C2E6900362570 /* SilentPushEventEmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FFF7E02E8C2E6100362570 /* SilentPushEventEmitter.swift */; }; 463BA791004B1B7AC1773914 /* Pods_Uniswap.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2226DF79BEAFECEE11A51347 /* Pods_Uniswap.framework */; }; - 4917B04DB81579CF4243A1DA /* Chain.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A8990A17F489F284E0DB1B3 /* Chain.graphql.swift */; }; - 4BB9E218D89B8AF5E4E27CCB /* NftOrderEdge.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49BD6C776029FAF4FC711DE7 /* NftOrderEdge.graphql.swift */; }; - 4C187202229BDC4D8898E067 /* Dimensions.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B1C71AED738D17898103A6 /* Dimensions.graphql.swift */; }; - 4EF8D293BB1EBCFBFC65A330 /* TokenTransfer.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DA4E7C052A6C4AC3A9DCDA6 /* TokenTransfer.graphql.swift */; }; - 4FED6AAF896BA371C56D88B1 /* Portfolio.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1849039FD4A210DA990363C8 /* Portfolio.graphql.swift */; }; - 50C89DFAF2DBC80D6343B164 /* NftAssetTraitInput.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 556339705BD103365D4ED07B /* NftAssetTraitInput.graphql.swift */; }; - 553E6B467BEE49C9984691C5 /* OnRampTransfer.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2139ABF56EA067ED792A48C9 /* OnRampTransfer.graphql.swift */; }; - 5804A1B1BB144D9EFBBE177D /* NftAssetEdge.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DF7C8888BE6AFA2F22889FE /* NftAssetEdge.graphql.swift */; }; + 47797825D18637A2FE57AC1F /* TokenProtectionInfoParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835D9DF76533D22EDF0E9D67 /* TokenProtectionInfoParts.graphql.swift */; }; + 4DB6B64CFF3B00FCF0258336 /* NftMarketplace.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83FB27D93A9468E6DB47231C /* NftMarketplace.graphql.swift */; }; + 4DB6D0FB611F6C68EADFB948 /* TokenProjectMarket.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B0505DB30407C221B6A8491 /* TokenProjectMarket.graphql.swift */; }; + 4DB88A2CBFF5FF356CA757F0 /* ApplicationContract.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59DD4D834D7B42A6D02F4F12 /* ApplicationContract.graphql.swift */; }; + 4FD78AA88D2EB8E22144BCDF /* NftAssetTraitInput.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F1731869775D16A9EAB475C /* NftAssetTraitInput.graphql.swift */; }; + 51362EC6F0E6D8928C067F5E /* NftBalancesFilterInput.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1810C60B56CC74150C868801 /* NftBalancesFilterInput.graphql.swift */; }; + 5551DDD6A8B9B28E03459816 /* TokenFeeDataParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1166529407CA13E4D4F24F12 /* TokenFeeDataParts.graphql.swift */; }; + 5676CCD265609B71D3B1DB7C /* NftCollectionEdge.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = F70593F94C8B315F5304B9BE /* NftCollectionEdge.graphql.swift */; }; + 57ECD348EA564F0CBF715E9E /* PortfolioBalancesQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0A53C3A09AD259B9A8EDBDD /* PortfolioBalancesQuery.graphql.swift */; }; 5B4398EC2DD3B22C00F6BE08 /* PrivateKeyDisplayManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 5B4398E82DD3B22C00F6BE08 /* PrivateKeyDisplayManager.m */; }; 5B4398ED2DD3B22C00F6BE08 /* PrivateKeyDisplayManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B4398E92DD3B22C00F6BE08 /* PrivateKeyDisplayManager.swift */; }; 5B4398EE2DD3B22C00F6BE08 /* PrivateKeyDisplayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B4398EA2DD3B22C00F6BE08 /* PrivateKeyDisplayView.swift */; }; 5B4CEC5F2DD65DD4009F082B /* CopyIconOutline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B4CEC5E2DD65DD4009F082B /* CopyIconOutline.swift */; }; - 5C3958DA046B5A84FE90C1BD /* FeeData.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17819A49DE69723EC60D9D72 /* FeeData.graphql.swift */; }; - 5D06BAB366D14A5AFE2355E8 /* TransactionType.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69F2FFD11FB5E462CC454A15 /* TransactionType.graphql.swift */; }; 5E5E0A632D380F5800E166AA /* Env.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E5E0A622D380F5700E166AA /* Env.swift */; }; 5EFB78362B1E585000E77EAC /* ConvertQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EFB78352B1E585000E77EAC /* ConvertQuery.graphql.swift */; }; - 614FCC3D8E9AA8175E950726 /* Currency.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25B4C7C7FEF32EAF10D030F9 /* Currency.graphql.swift */; }; - 61988A922656857278F7CBF9 /* TokenBalanceMainParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 137696908D703632CEE3AB28 /* TokenBalanceMainParts.graphql.swift */; }; - 62B32E9623DA0E939F062033 /* TokensQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3C3F0403FAEFDD632AC6CCE /* TokensQuery.graphql.swift */; }; - 63BDDE4499AC6B2375C9F795 /* FavoriteTokenCardQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F0EEAC1DADE827AE38EB8A6 /* FavoriteTokenCardQuery.graphql.swift */; }; - 648B919F00D069DE1F0040F6 /* NftMarketplace.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 600250B00B6C02AAB063818B /* NftMarketplace.graphql.swift */; }; + 6250D4ACD5696D845FD83DFD /* HomeScreenTokensQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7FF51A507BF788E64E3EFE /* HomeScreenTokensQuery.graphql.swift */; }; + 63FBF9C23ED568816590F093 /* ActivityDetails.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 075E4C1DFAA00EE5B3CE792D /* ActivityDetails.graphql.swift */; }; 649A7A782D9AE70B00B53589 /* KeychainUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649A7A772D9AE70B00B53589 /* KeychainUtils.swift */; }; 649A7A792D9AE70B00B53589 /* KeychainConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649A7A762D9AE70B00B53589 /* KeychainConstants.swift */; }; - 66D2765A00D8CE3136D28F1F /* SafetyLevel.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACFA5B4204741038C986C5D /* SafetyLevel.graphql.swift */; }; - 677FC15A05D9BD23930FAC95 /* TokenQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74885865ED8CFEA875E40916 /* TokenQuery.graphql.swift */; }; - 6A60BDC9D46A710D871DEC6E /* NftProfile.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB35E518AD7FBFEBEA9CAB95 /* NftProfile.graphql.swift */; }; + 6882C4768011FDD4D746BDDB /* TokenBasicInfoParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = D13E592852F52B2855F05A5E /* TokenBasicInfoParts.graphql.swift */; }; 6BC7D07E2B5FF02400617C95 /* ScantasticEncryption.m in Sources */ = {isa = PBXBuildFile; fileRef = 6BC7D07B2B5FF02400617C95 /* ScantasticEncryption.m */; }; 6BC7D07F2B5FF02400617C95 /* ScantasticEncryption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BC7D07C2B5FF02400617C95 /* ScantasticEncryption.swift */; }; 6BC7D0802B5FF02400617C95 /* EncryptionUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BC7D07D2B5FF02400617C95 /* EncryptionUtils.swift */; }; @@ -198,21 +195,26 @@ 6CA91BE12A95226200C4063E /* RNCloudStorageBackupsManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 6CA91BDE2A95226200C4063E /* RNCloudStorageBackupsManager.m */; }; 6CA91BE22A95226200C4063E /* EncryptionHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CA91BDF2A95226200C4063E /* EncryptionHelper.swift */; }; 6CA91BE32A95226200C4063E /* RNCloudStorageBackupsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CA91BE02A95226200C4063E /* RNCloudStorageBackupsManager.swift */; }; + 70CD2D0665E5AC6A0DF6F697 /* NFTItemScreenQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03AB6B21A1BFB3A18982B47E /* NFTItemScreenQuery.graphql.swift */; }; 70EB8338CA39744B7DBD553E /* Pods_WidgetIntentExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1064E23E366D0C2C2B20C30E /* Pods_WidgetIntentExtension.framework */; }; - 71CF37F19F1C138B57CC135C /* TopTokenParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5640432978873FB030467750 /* TopTokenParts.graphql.swift */; }; + 71954B20E4341CEC36203F87 /* NftActivityEdge.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 043C7D80D6722AFCF1715D07 /* NftActivityEdge.graphql.swift */; }; + 7700BED7CD7F52C83A3FF430 /* NftAssetEdge.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFFD4C1E5A4B30DE3CE02DB4 /* NftAssetEdge.graphql.swift */; }; 77CF6065C8A24FE48204A2C1 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BF9176E944C84910B1C0B057 /* SplashScreen.storyboard */; }; - 7BE97D20155AE69FF36C4CB4 /* NftStandard.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B308EC3DB7C3C1DD5DDE03D /* NftStandard.graphql.swift */; }; - 818179F5724AA35E1B1D5A9C /* ProtectionInfo.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0702203B17BDD858CED28F8B /* ProtectionInfo.graphql.swift */; }; + 7B557C3224F990851430DBD2 /* Portfolio.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5397E77A577E9952D2477020 /* Portfolio.graphql.swift */; }; + 7C886F9D4C02EF4E5F67B011 /* NftsQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE735ED4766E65C0D6808F9A /* NftsQuery.graphql.swift */; }; + 7E3412EB776E1F43D6904BA0 /* PageInfo.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97FBBE677B56FFE07052EF1D /* PageInfo.graphql.swift */; }; + 8117BFB02DCF0F315FD83E67 /* TokenBalance.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 181209B4CDE80BDC80891CFD /* TokenBalance.graphql.swift */; }; + 8141B38CCDB09F91180C0EBD /* NftCollectionConnection.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D09FFE2B5E56CDCAB2F9455 /* NftCollectionConnection.graphql.swift */; }; + 81E55E8D3056E2378A332B31 /* TokenDetailsScreenQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 957A35D2DE7A140BA198A0F1 /* TokenDetailsScreenQuery.graphql.swift */; }; 8273FC23FB1AE47B80C5E09F /* Pods_OneSignalNotificationServiceExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 15092E550A1C78508ABA3280 /* Pods_OneSignalNotificationServiceExtension.framework */; }; 8385A47D3C765B841F450090 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C26D739993D5C939C6FBB58A /* ExpoModulesProvider.swift */; }; - 8410B98A8D7974A941AAF299 /* WidgetTokensQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA33683D2BA6BBE3251D67C /* WidgetTokensQuery.graphql.swift */; }; - 85ADD03923353DB3D6CD7301 /* SwapOrderStatus.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = D574AB5194929038A54B4D4B /* SwapOrderStatus.graphql.swift */; }; - 85D0E81B798D0F2D841386E9 /* NftCollectionMarket.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F4F47D22B749A438B5BF674 /* NftCollectionMarket.graphql.swift */; }; - 869F3639FBC6D8156FFE3BD3 /* TokenProjectMarket.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B9A505509D9A85BE1DB7D05 /* TokenProjectMarket.graphql.swift */; }; - 8950F8354810AA673E1E35DA /* SchemaMetadata.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = C65195A1CC9894C6341385C3 /* SchemaMetadata.graphql.swift */; }; - 8ADDD2E2AB1FD9D1D9AF5627 /* BlockaidFees.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD35300F81746B2D4A23065A /* BlockaidFees.graphql.swift */; }; - 8CC06A8205186D0640F0BC55 /* NftAssetConnection.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 218B8AB05C80D919671D3970 /* NftAssetConnection.graphql.swift */; }; - 8CEA6459A5B739C3A0382000 /* TokenProjectsQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8A49D011C60137D4034CAC4 /* TokenProjectsQuery.graphql.swift */; }; + 845327D60EFBB850189D6AB3 /* Amount.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C3461E088654B80ABE3DE7 /* Amount.graphql.swift */; }; + 86038259E487A108DDC2948B /* Image.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C9047B981FA322A727947F7 /* Image.graphql.swift */; }; + 8971E73E041A6283E2684481 /* TokenProject.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8856DFFC7FDC9BF105B0B4E3 /* TokenProject.graphql.swift */; }; + 8B2A92172EB3E78E00990413 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B2A92162EB3E78E00990413 /* AppDelegate.swift */; }; + 8BCFF1F648887F78894F1071 /* ContractInput.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = AED050A208757F137DBDA57D /* ContractInput.graphql.swift */; }; + 8D85F349FEF81A52FB93EAAB /* AssetChange.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D4C849A7805FCCEE8C8C3E6 /* AssetChange.graphql.swift */; }; + 8E6DA65AC5CAE9F9A67F9E38 /* TopTokensQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 243576FAC8D2801F9D99ECC9 /* TopTokensQuery.graphql.swift */; }; 8E89C3AE2AB8AAA400C84DE5 /* MnemonicConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E89C3A62AB8AAA400C84DE5 /* MnemonicConfirmationView.swift */; }; 8E89C3AF2AB8AAA400C84DE5 /* MnemonicDisplayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E89C3A72AB8AAA400C84DE5 /* MnemonicDisplayView.swift */; }; 8E89C3B12AB8AAA400C84DE5 /* MnemonicConfirmationWordBankView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E89C3A92AB8AAA400C84DE5 /* MnemonicConfirmationWordBankView.swift */; }; @@ -227,7 +229,7 @@ 8EBFB1552ABA6AA6006B32A8 /* PasteIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EBFB1542ABA6AA6006B32A8 /* PasteIcon.swift */; }; 8ED0562C2AA78E2C009BD5A2 /* ScrollFadeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8ED0562B2AA78E2C009BD5A2 /* ScrollFadeExtensions.swift */; }; 8EE7C0582AFD7B2100E0D9CD /* DescriptionTranslations.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EE7C0572AFD7B2100E0D9CD /* DescriptionTranslations.graphql.swift */; }; - 8FE0F30936E893297558F467 /* ProtectionResult.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD0FC2D534CB82B151C7F9B7 /* ProtectionResult.graphql.swift */; }; + 9061ACE5AF99CEDE7BA51C94 /* NftProfile.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE2A9FBFB7F718246B668A60 /* NftProfile.graphql.swift */; }; 9127D1362CC2D3D00096F134 /* TokenBalanceMainParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9127D1342CC2D3D00096F134 /* TokenBalanceMainParts.graphql.swift */; }; 9127D1372CC2D3D00096F134 /* TokenBalanceQuantityParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9127D1352CC2D3D00096F134 /* TokenBalanceQuantityParts.graphql.swift */; }; 9173CEBC2D03C6F30036DA28 /* TokenBalanceParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9173CEBB2D03C6F30036DA28 /* TokenBalanceParts.graphql.swift */; }; @@ -243,15 +245,14 @@ 91D501792CDBEAE700B09B7F /* TopTokenParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91D5016E2CDBEAE700B09B7F /* TopTokenParts.graphql.swift */; }; 91D5017A2CDBEAE700B09B7F /* TokenFeeDataParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91D5016F2CDBEAE700B09B7F /* TokenFeeDataParts.graphql.swift */; }; 91D5017E2CDBEAF600B09B7F /* HomeScreenTokenParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91D5017C2CDBEAF600B09B7F /* HomeScreenTokenParts.graphql.swift */; }; - 9301D18644F3DFA9ABB8F0BE /* TokenApproval.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E975BEDBED5FE51D0DC6E96 /* TokenApproval.graphql.swift */; }; - 93566FBDE94E1A2D8CC5AB62 /* ConvertQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD3F0794908635CDFA7DAB6 /* ConvertQuery.graphql.swift */; }; - 9381B5EA5839DA17D07A45F0 /* TokenDetailsScreenQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7A33933670CCAD9E62A1A80 /* TokenDetailsScreenQuery.graphql.swift */; }; - 93AEECDBDB160B5E0C9E3149 /* TokenProjectDescriptionQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FF8B660E9C2C0D8DDF7588A /* TokenProjectDescriptionQuery.graphql.swift */; }; - 97CA219E1FFD832D8FA02C20 /* TransactionDetails.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABE51731EB853137302A7B0D /* TransactionDetails.graphql.swift */; }; - 9822D243ED2F5C350404A375 /* HomeScreenTokenParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27F8B915042BD8D67D9627EA /* HomeScreenTokenParts.graphql.swift */; }; - 9A3E861F5D7B0F2CB0EFA7A6 /* AssetActivity.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4624A41EC1468712BD77653 /* AssetActivity.graphql.swift */; }; - 9AF0D1FDF2BFB17FB732C5FD /* SchemaConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E49E724432C70A2551FB2C7 /* SchemaConfiguration.swift */; }; - 9B6C88F7D8D542AAC353A3EA /* NftOrderConnection.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39FC2A13550FA6754EC1A7FC /* NftOrderConnection.graphql.swift */; }; + 91DBDA9B4F4006350AEB8E4B /* NftStandard.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0373C61A7AF015058A502CE2 /* NftStandard.graphql.swift */; }; + 9260D04A891FEDE2BF511707 /* Currency.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD0CA37FB709C06F123EBF74 /* Currency.graphql.swift */; }; + 997BFD5324E4BDE2422FEFED /* NftAssetsFilterInput.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 310373063D8A11E9E02314FE /* NftAssetsFilterInput.graphql.swift */; }; + 9A9B39BC22F3BDFCD0917B66 /* OffRampTransactionDetails.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CA32B2B2997D73A0185635 /* OffRampTransactionDetails.graphql.swift */; }; + 9D638326CD705ABE549C8CA7 /* TokenProjectMarketsParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C2E70E43A02405C5CEAD7C /* TokenProjectMarketsParts.graphql.swift */; }; + 9E7EC26AC45301198E280DC7 /* NftOrderEdge.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD158681F2E84A5E49656B11 /* NftOrderEdge.graphql.swift */; }; + 9EBC41A90B1A80653960045E /* SchemaMetadata.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 522135E54BDB258B45B5A694 /* SchemaMetadata.graphql.swift */; }; + 9EC9E4AB5C3FD254EDB28D7A /* PortfolioValueModifier.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E09D12605BAA868AB51573BE /* PortfolioValueModifier.graphql.swift */; }; 9F00A43A2B33894C0088A0D0 /* ApplicationContract.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F00A4392B33894C0088A0D0 /* ApplicationContract.graphql.swift */; }; 9F29D4ED2B47126D004D003A /* NftBalanceAssetInput.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F29D4EC2B47126D004D003A /* NftBalanceAssetInput.graphql.swift */; }; 9F78980B2A819CC4004D5A98 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 072F6C222A44A32E00DA720A /* SwiftUI.framework */; }; @@ -267,73 +268,74 @@ 9FCEBF002A95A8E00079EDDB /* RNWalletConnect.m in Sources */ = {isa = PBXBuildFile; fileRef = 9FCEBEFE2A95A8E00079EDDB /* RNWalletConnect.m */; }; 9FCEBF012A95A8E00079EDDB /* RNWalletConnect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FCEBEFF2A95A8E00079EDDB /* RNWalletConnect.swift */; }; 9FCEBF042A95A99C0079EDDB /* RCTThemeModule.m in Sources */ = {isa = PBXBuildFile; fileRef = 9FCEBF032A95A99B0079EDDB /* RCTThemeModule.m */; }; - 9FEC9B8B2A858CF1003CD019 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 9FEC9B8A2A858CF1003CD019 /* AppDelegate.m */; }; - A104024A1861354EC1DD53C1 /* PortfolioBalancesQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 985DDC8324B872DD44E87781 /* PortfolioBalancesQuery.graphql.swift */; }; + A250CF455F6CEFB64ABFC277 /* HomeScreenTokenParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB772B265A35BFBC3A7761AD /* HomeScreenTokenParts.graphql.swift */; }; A32F9FBD272343C9002CFCDB /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = A32F9FBC272343C8002CFCDB /* GoogleService-Info.plist */; }; - A3318C676D7FBF78A1583B16 /* NftCollectionEdge.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37038CCEB11E70E2027EF4EA /* NftCollectionEdge.graphql.swift */; }; A3551F2CAC134AD49D40927F /* Basel-Grotesk-Book.otf in Resources */ = {isa = PBXBuildFile; fileRef = 6F33E8069B7B40AFB313B8B0 /* Basel-Grotesk-Book.otf */; }; A3F0A5B1272B1DFA00895B25 /* KeychainSwiftDistrib.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3F0A5B0272B1DFA00895B25 /* KeychainSwiftDistrib.swift */; }; A70E4DD42C25DA0A002D6D86 /* NetworkFee.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70E4DD32C25DA0A002D6D86 /* NetworkFee.graphql.swift */; }; A70E4DD72C260416002D6D86 /* SwapOrderType.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70E4DD52C260416002D6D86 /* SwapOrderType.graphql.swift */; }; A70E4DD82C260416002D6D86 /* SwapOrderStatus.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70E4DD62C260416002D6D86 /* SwapOrderStatus.graphql.swift */; }; A7B8EFCB2BF68F0D00CA4A1C /* FeeData.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7B8EFCA2BF68F0D00CA4A1C /* FeeData.graphql.swift */; }; - A974633048E27D5D23420F34 /* TokenBasicInfoParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3377F7BB38BD5A45AE31909D /* TokenBasicInfoParts.graphql.swift */; }; - AA3AE4E5C2AAC3F9460A3637 /* NftBalanceAssetInput.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5FA43C5E31035775917C4E /* NftBalanceAssetInput.graphql.swift */; }; + A88A26642AC25B022F428953 /* OnRampServiceProvider.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BED80DB034487FCA1450EDB /* OnRampServiceProvider.graphql.swift */; }; + A949039A6A9EB3584B5644F3 /* IAmount.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37376E4CF3A4B76E9DAE150C /* IAmount.graphql.swift */; }; + A9AF7B483E9666E88CD6253E /* NftOrderConnection.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98F489175099FE697721272D /* NftOrderConnection.graphql.swift */; }; + AAB837C01239A62D00068853 /* TransactionType.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18D48B25E03122149B0D1304 /* TransactionType.graphql.swift */; }; AC0EE0982BD826E700BCCF07 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = AC0EE0972BD826E700BCCF07 /* PrivacyInfo.xcprivacy */; }; AC2EF4032C914B1600EEEFDB /* fonts in Resources */ = {isa = PBXBuildFile; fileRef = AC2EF4022C914B1600EEEFDB /* fonts */; }; - AC70FF8207ED26561B634E30 /* NftsQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9030AC59702756C39396FFE /* NftsQuery.graphql.swift */; }; - ADE104A101B3DFFBDB308189 /* Query.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED157D3DB73302C8A1B9522E /* Query.graphql.swift */; }; - AF83E7713BB625D787DD1A1D /* TokenMarket.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBD833F6580F9B82A68D4A98 /* TokenMarket.graphql.swift */; }; + ACA7AE75760B76F7B30DCD43 /* AmountChange.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 066BBBD815CF2DE8803338BC /* AmountChange.graphql.swift */; }; + AD6036D32C44048781B728E1 /* SchemaConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF4115ACC8D8F0C7529AABA7 /* SchemaConfiguration.swift */; }; B193AD315CF844A3BDC3D11D /* Basel-Grotesk-Medium.otf in Resources */ = {isa = PBXBuildFile; fileRef = 3C606D2C81014A0A8898F38E /* Basel-Grotesk-Medium.otf */; }; - B377DB0418EA15695F208D9F /* NetworkFee.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BEADEB4AE05B860CF2232D1 /* NetworkFee.graphql.swift */; }; - B61E182CF4937B5169885C95 /* OnRampTransactionDetails.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12D3AB6C2FC9513E9F224B7C /* OnRampTransactionDetails.graphql.swift */; }; - B746C09DA19B4C7F9C700989 /* NftActivityConnection.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = F912AB9AB67808B740246798 /* NftActivityConnection.graphql.swift */; }; - BA83003638263D702D03C3C1 /* ActivityDetails.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A4EB7BAD2C90E61FD6C30AF /* ActivityDetails.graphql.swift */; }; + B64BD6DEB100AEB723DB640D /* NftBalanceConnection.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DDC1F42E7172783EAF3776 /* NftBalanceConnection.graphql.swift */; }; BA869E372D56B0B600D7A718 /* WidgetTokensQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA869E362D56B0B600D7A718 /* WidgetTokensQuery.graphql.swift */; }; BA8FC9627A40644259D9E2F9 /* Pods_WidgetsCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CB29AC0C0907A833F23D2C30 /* Pods_WidgetsCore.framework */; }; - BD1FF76A8E50EFEA0720CC04 /* NftAsset.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2932B486DD7070DF226A27B /* NftAsset.graphql.swift */; }; - BD59A9C8414D6A4BFE9C9A2E /* NftActivity.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1A24D364C0D262A14BB7182 /* NftActivity.graphql.swift */; }; - C1FC1F8B4A2725A195F66D60 /* PortfolioValueModifier.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41003491EF939043CF9D0F4E /* PortfolioValueModifier.graphql.swift */; }; - C791C5505DBB3036B9D5066D /* SwapOrderType.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 148617FD73F2CD7117960DBA /* SwapOrderType.graphql.swift */; }; - C7ABAADA504107D152A52FD4 /* NftBalancesFilterInput.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41BA2129FFE961C8549C8A30 /* NftBalancesFilterInput.graphql.swift */; }; - C8338C5BE951EF9111C48217 /* TokenBasicProjectParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C2F4BE3080D4129D4478528 /* TokenBasicProjectParts.graphql.swift */; }; - C8402101FF510BAA358DCA46 /* TokenBalanceParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C34E94906CB9F0A016D566 /* TokenBalanceParts.graphql.swift */; }; - CA0EDABCA1B6C2B0936BF5CF /* NftBalanceEdge.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B132543F80E3C9191219E6D /* NftBalanceEdge.graphql.swift */; }; - CCBC45FD4310D8153D859361 /* TransactionStatus.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2ABA47847A73130D356EB6BF /* TransactionStatus.graphql.swift */; }; - CE1DF567D58943ACCBB8360C /* Amount.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20E9461A6C9C1CD6A24BEED /* Amount.graphql.swift */; }; - CF2BAECCF9A2EC43AC3EC583 /* OnRampServiceProvider.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A84A192F750A0F1BC8D7A69 /* OnRampServiceProvider.graphql.swift */; }; - CF36F741187285B430EFE557 /* NftBalance.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05BE2F49DD1FB9A692E21C13 /* NftBalance.graphql.swift */; }; + BAB54489223FD0027F7A8DBE /* Dimensions.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 764C4757EB194D13A8857DFA /* Dimensions.graphql.swift */; }; + BB28AF35DC102F8DE2A4BE9D /* OnRampTransactionsAuth.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3CC3D9DE64A39897BA580B3 /* OnRampTransactionsAuth.graphql.swift */; }; + BE0DBD01CF4DCA6D3CDDA369 /* NftActivity.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55C67E53A2C8AEB6AE92E33F /* NftActivity.graphql.swift */; }; + BED0DD22C0D9E09C85DF010D /* NftAsset.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9F3B31FC6AF8CE30CD7DA2 /* NftAsset.graphql.swift */; }; + BFB114718A0D262E3AEAEDC0 /* TransactionStatus.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 945EF0A3BD6F19DA14E40AE1 /* TransactionStatus.graphql.swift */; }; + C064037906B259088B6B78B2 /* NetworkFee.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09BE4B220A4F8465C3D01BA5 /* NetworkFee.graphql.swift */; }; + C0BBEE1CAEA4B2F778426BDE /* SelectWalletScreenQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB9CACD175D6C20B6D4707C8 /* SelectWalletScreenQuery.graphql.swift */; }; + C8F5AF75BDB439143FE1C173 /* NftActivityFilterInput.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C3E075E850CEC1714BE52C /* NftActivityFilterInput.graphql.swift */; }; + CA966CBD02A7B16BC55F1B8E /* ProtectionResult.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAB6B8C64F7525D0D0E978C6 /* ProtectionResult.graphql.swift */; }; + CBEB9122D993BCB0E9A604B7 /* Query.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5956ECBE73216D266D8D2E86 /* Query.graphql.swift */; }; + CE8C365D91CADE7A5A8F4D52 /* TopTokenParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07F0E504152E22110917353E /* TopTokenParts.graphql.swift */; }; + CEE9E9912D5621F6E8F819B7 /* MultiplePortfolioBalancesQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 595699514939A2CE780AF0CF /* MultiplePortfolioBalancesQuery.graphql.swift */; }; + D0B9B6DEC559290B7F64B24F /* TokenQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0914ECF70C274E2B064BCD42 /* TokenQuery.graphql.swift */; }; + D1FB4E293EC152C12A495ACD /* TokenStandard.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = D34AC1057AD14362BB91E88B /* TokenStandard.graphql.swift */; }; D3B63ACA9B0C42F68080B080 /* InputMono-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 1834199AFFB04D91B05FFB64 /* InputMono-Regular.ttf */; }; - D660CA59775C3634324037ED /* MultiplePortfolioBalancesQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4429CA6DA254ECC24A65DA14 /* MultiplePortfolioBalancesQuery.graphql.swift */; }; - D680C4844F2C5DACF5D0892E /* NftAssetsFilterInput.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B477EBEA1EDA3914C77E8A6 /* NftAssetsFilterInput.graphql.swift */; }; + D48D1B7A4469BAAA22608058 /* TokenSortableField.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F133663B435829DE47E3CE /* TokenSortableField.graphql.swift */; }; + D5F36D3EDC206DF15EC368AC /* NftActivityType.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CDAC035887C3908C3BD1A77 /* NftActivityType.graphql.swift */; }; + D6149AE9ED70F546DF5841BD /* ConvertQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E4DEE7FC8D7BCD030CFA4D3 /* ConvertQuery.graphql.swift */; }; D7926D4A878B2237137B300F /* Pods_WidgetsCoreTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 021E59CE7ECBD4FE0F3BFCFD /* Pods_WidgetsCoreTests.framework */; }; - D7E3642851C618D369D26B82 /* TokenMarketParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC36D963A2A52D1E7CA77E4 /* TokenMarketParts.graphql.swift */; }; - D81BAFFCC105668B88B607FC /* NftCollectionConnection.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92DE390311705FF1EE65C0E6 /* NftCollectionConnection.graphql.swift */; }; - DC0641C5870F91D26C914F1D /* TokenPriceHistoryQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DFBD29C0BDFDB4464B8189C /* TokenPriceHistoryQuery.graphql.swift */; }; + D8A0C6D04FF53BA4F50543FE /* NftAssetTrait.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85B43D489CC5ACF1EA34E9EB /* NftAssetTrait.graphql.swift */; }; + DB752E8F664505726ABDBCD3 /* NftBalance.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2B02717B1A87BD41920151 /* NftBalance.graphql.swift */; }; + DC4AA6DE28B9F40A3D7600F1 /* TokenProjectsQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98CEFE7189D63880FD5A702 /* TokenProjectsQuery.graphql.swift */; }; DE2F24512E7204C2CA255C50 /* Pods_Widgets.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2E8B7D36D2E14D9488F351EB /* Pods_Widgets.framework */; }; - E091F379106E92D3181103CB /* IAmount.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03AD8C2B1226EBE1524F2573 /* IAmount.graphql.swift */; }; - E0F63BA3A99DB10FA1CCAB5E /* TokenProjectUrlsParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 236126979349098B98C70F27 /* TokenProjectUrlsParts.graphql.swift */; }; - E24ED8688E9B18BBD543F8F0 /* TokenBalance.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4B149117182C6CC58DD570C /* TokenBalance.graphql.swift */; }; + E1009979B48ACF5017C12F09 /* TransactionListQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95283AC560844B50E95934E4 /* TransactionListQuery.graphql.swift */; }; + E2C528F617A9675690171D54 /* DescriptionTranslations.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = D91432EF6C35F5A6301340A2 /* DescriptionTranslations.graphql.swift */; }; + E475BA358DD8BE30A6FDC051 /* NftBalanceEdge.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DD55DFFC717D37DD960B82E /* NftBalanceEdge.graphql.swift */; }; + E4985BA9090E013F2C9FC190 /* TokenMarket.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8E5B09464C768590127BFA6 /* TokenMarket.graphql.swift */; }; E4B3067A930D2E57558E5229 /* Pods_Uniswap_UniswapTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0929C0B4AE1570B8C0B45D4D /* Pods_Uniswap_UniswapTests.framework */; }; - E54093DC857B8C634804D42B /* NftTransfer.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3317EBD4E3B11E2F2A1CE58 /* NftTransfer.graphql.swift */; }; - EF05F69E61EA8A16C6A66D53 /* IContract.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E0D68CC8D1F13D864F069BB /* IContract.graphql.swift */; }; - EF3D92CA76DEE66090F18D0C /* NftCollectionScreenQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE0973BB445AAD8FAA25757F /* NftCollectionScreenQuery.graphql.swift */; }; - F0D6A92BB4FCE19CDFA5BBE1 /* HomeScreenTokensQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7E03AA02B39A7A90C96AA76 /* HomeScreenTokensQuery.graphql.swift */; }; - F2EB62621E64B57B07DE8B13 /* Token.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647933A5DC35BDFEEF3620A1 /* Token.graphql.swift */; }; + E654760CA0FF19139A85472A /* TokensQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4C8174A3A9EB3244AA844F4 /* TokensQuery.graphql.swift */; }; + E7B6F1CA0E30585C949C9D9A /* NftAssetConnection.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD86EFAA0D6B8FBECCC4828B /* NftAssetConnection.graphql.swift */; }; + E7D4A29333634717D099F80E /* SwapOrderType.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AFDF569E43F173C2F5A07AF /* SwapOrderType.graphql.swift */; }; + E7EDBB8CDF65D5D6602BF8FC /* NftOrder.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67C0D95F7EA9E7ACA9B18692 /* NftOrder.graphql.swift */; }; + EB0A75424F8EEF6612D28D52 /* NftTransfer.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74AD16E33DC518A42C386FE2 /* NftTransfer.graphql.swift */; }; + ED606CD83873CC9DFCCA44F1 /* FavoriteTokenCardQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B03A0A6AB0957A512A2D225 /* FavoriteTokenCardQuery.graphql.swift */; }; + EE4B861AE191A1BF70CF3784 /* WidgetTokensQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F0A91F56B1CA2E71B479BAD /* WidgetTokensQuery.graphql.swift */; }; + F09E3F15A36A9050B028B3E3 /* TokenBasicProjectParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E8286984864AD69A0FAE3BC /* TokenBasicProjectParts.graphql.swift */; }; + F1193089AE71CA3C4C101EA5 /* OffRampTransfer.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 166A9AE7203545C397E683E8 /* OffRampTransfer.graphql.swift */; }; F35AFD3E27EE49990011A725 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F35AFD3D27EE49990011A725 /* NotificationService.swift */; }; F35AFD4227EE49990011A725 /* OneSignalNotificationServiceExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = F35AFD3B27EE49990011A725 /* OneSignalNotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - F53121FB1B070DB9A81347DF /* TokenProjectMarketsParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC055C3EDFDE2966FCA83C9E /* TokenProjectMarketsParts.graphql.swift */; }; - F5B577CA1201CB6FF04A9A58 /* ApplicationContract.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 608B07304ED1387B5AEAA3BB /* ApplicationContract.graphql.swift */; }; - F772B09295DABC5E8895C1C5 /* SelectWalletScreenQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = B033206DC974BCB6AF8CC613 /* SelectWalletScreenQuery.graphql.swift */; }; - FA8EB6A63AB3F56194C6CFB8 /* TokenBalanceQuantityParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2370BAD675DC8064C00AC037 /* TokenBalanceQuantityParts.graphql.swift */; }; - FBE634E2AEC7A62B89863366 /* ContractInput.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7B3C6DF5DED22E892851B10 /* ContractInput.graphql.swift */; }; - FC7C117CEA5EA63460E6A2C6 /* NftApproveForAll.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5BCBB5D46312050E4A5BD64 /* NftApproveForAll.graphql.swift */; }; - FD4E55146E046C7F5983A379 /* FeedTransactionListQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BA0ACE5B9B2C41187CD41F3 /* FeedTransactionListQuery.graphql.swift */; }; + F49C1C4E9175DFE7EEF9FB51 /* Token.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FB1590D18A58D37C750D9AC /* Token.graphql.swift */; }; + F77F1A182DCA1D8DFE46ACC8 /* TokenBalanceParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65AD13BFAD4EBA18AA6205E3 /* TokenBalanceParts.graphql.swift */; }; + F814C0144D90ADB4A8E0A34E /* Chain.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DBCFBC330BF97BA2F630908 /* Chain.graphql.swift */; }; + F86D86FC31BBDA7246C50392 /* ProtectionAttackType.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3F4D5E8B9ED6E0FE29EA779 /* ProtectionAttackType.graphql.swift */; }; + FA318FB37223FAF04A5C887B /* TokenProjectUrlsParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 190523DBBA423868C74FF80D /* TokenProjectUrlsParts.graphql.swift */; }; + FC61EDE606C85346CE069C5D /* TransactionHistoryUpdaterQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 559CBC7F568C769C48E7C6F4 /* TransactionHistoryUpdaterQuery.graphql.swift */; }; FD54D51D296C79A4007A37E9 /* GoogleServiceInfo in Resources */ = {isa = PBXBuildFile; fileRef = FD54D51C296C79A4007A37E9 /* GoogleServiceInfo */; }; FD7304CE28A364FC0085BDEA /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FD7304CD28A364FC0085BDEA /* Colors.xcassets */; }; FD7304D028A3650A0085BDEA /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7304CF28A3650A0085BDEA /* Colors.swift */; }; - FD84AA379C4562598807843D /* TokenStandard.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC419570F5A025E80AB10A72 /* TokenStandard.graphql.swift */; }; - FE9CE5C3B5A456B249FC34C5 /* NftActivityEdge.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DB4379B5E222207CA7328D0 /* NftActivityEdge.graphql.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -436,16 +438,16 @@ 00E356EE1AD99517003FC87E /* UniswapTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UniswapTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 00E356F21AD99517003FC87E /* UniswapTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UniswapTests.m; sourceTree = ""; }; - 018603D0FA4D05DBF16F1441 /* TokenFeeDataParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenFeeDataParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TokenFeeDataParts.graphql.swift; sourceTree = ""; }; 021E59CE7ECBD4FE0F3BFCFD /* Pods_WidgetsCoreTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_WidgetsCoreTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 0373C61A7AF015058A502CE2 /* NftStandard.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftStandard.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/NftStandard.graphql.swift; sourceTree = ""; }; 037C5AA92C04970B00B1D808 /* CopyIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyIcon.swift; sourceTree = ""; }; - 03AD8C2B1226EBE1524F2573 /* IAmount.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = IAmount.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Interfaces/IAmount.graphql.swift; sourceTree = ""; }; + 03AB6B21A1BFB3A18982B47E /* NFTItemScreenQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NFTItemScreenQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/NFTItemScreenQuery.graphql.swift; sourceTree = ""; }; 03C788222C10E7390011E5DC /* ActionButtons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButtons.swift; sourceTree = ""; }; 03D2F3172C218D380030D987 /* RelativeOffsetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelativeOffsetView.swift; sourceTree = ""; }; - 04E5C2900254E7BA7F10CE51 /* TopTokensQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TopTokensQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/TopTokensQuery.graphql.swift; sourceTree = ""; }; - 05BE2F49DD1FB9A692E21C13 /* NftBalance.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftBalance.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftBalance.graphql.swift; sourceTree = ""; }; + 043C7D80D6722AFCF1715D07 /* NftActivityEdge.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftActivityEdge.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftActivityEdge.graphql.swift; sourceTree = ""; }; + 05C66C49AF2B78703FCE4C7C /* AssetActivity.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AssetActivity.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/AssetActivity.graphql.swift; sourceTree = ""; }; 065A981F892F7A06A900FCD5 /* Pods-WidgetsCoreTests.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetsCoreTests.dev.xcconfig"; path = "Target Support Files/Pods-WidgetsCoreTests/Pods-WidgetsCoreTests.dev.xcconfig"; sourceTree = ""; }; - 0702203B17BDD858CED28F8B /* ProtectionInfo.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ProtectionInfo.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/ProtectionInfo.graphql.swift; sourceTree = ""; }; + 066BBBD815CF2DE8803338BC /* AmountChange.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AmountChange.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/AmountChange.graphql.swift; sourceTree = ""; }; 0703EE022A5734A600AED1DA /* UserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaults.swift; sourceTree = ""; }; 070480372A58A507009006CE /* WidgetIntentExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WidgetIntentExtension.entitlements; sourceTree = ""; }; 0712B3629C74D1F958DF35FB /* Pods-Uniswap-UniswapTests.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Uniswap-UniswapTests.dev.xcconfig"; path = "Target Support Files/Pods-Uniswap-UniswapTests/Pods-Uniswap-UniswapTests.dev.xcconfig"; sourceTree = ""; }; @@ -544,6 +546,7 @@ 074321E92A83E3C900F8518D /* IAmount.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IAmount.graphql.swift; sourceTree = ""; }; 074321EA2A83E3C900F8518D /* IContract.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IContract.graphql.swift; sourceTree = ""; }; 0743223F2A841BBD00F8518D /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; + 075E4C1DFAA00EE5B3CE792D /* ActivityDetails.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ActivityDetails.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Unions/ActivityDetails.graphql.swift; sourceTree = ""; }; 0767E0372A65C8330042ADA2 /* Colors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Colors.swift; sourceTree = ""; }; 0767E03A2A65D2550042ADA2 /* Styling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Styling.swift; sourceTree = ""; }; 077E60382A85587800ABC4B9 /* TokensQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokensQuery.graphql.swift; sourceTree = ""; }; @@ -556,17 +559,20 @@ 07B0676A2A7D6EC8001DD9B9 /* RNWidgets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RNWidgets.swift; sourceTree = ""; }; 07B0676B2A7D6EC8001DD9B9 /* RNWidgets.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNWidgets.m; sourceTree = ""; }; 07F0C28E2A5F3E2E00D5353E /* Env.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Env.swift; sourceTree = ""; }; + 07F0E504152E22110917353E /* TopTokenParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TopTokenParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TopTokenParts.graphql.swift; sourceTree = ""; }; 07F1363F2A575EC00067004F /* DataQueries.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataQueries.swift; sourceTree = ""; }; 07F136412A5763480067004F /* Network.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Network.swift; sourceTree = ""; }; 07F5CF702A6AD97D00C648A5 /* Chart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Chart.swift; sourceTree = ""; }; 07F5CF742A7020FD00C648A5 /* Format.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Format.swift; sourceTree = ""; }; 08C60D53AB82A6D0D31D0F78 /* Pods-WidgetIntentExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetIntentExtension.release.xcconfig"; path = "Target Support Files/Pods-WidgetIntentExtension/Pods-WidgetIntentExtension.release.xcconfig"; sourceTree = ""; }; 08EBF075A4482F701892270B /* Pods-Widgets.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Widgets.dev.xcconfig"; path = "Target Support Files/Pods-Widgets/Pods-Widgets.dev.xcconfig"; sourceTree = ""; }; + 0914ECF70C274E2B064BCD42 /* TokenQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/TokenQuery.graphql.swift; sourceTree = ""; }; 0929C0B4AE1570B8C0B45D4D /* Pods_Uniswap_UniswapTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Uniswap_UniswapTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 0B132543F80E3C9191219E6D /* NftBalanceEdge.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftBalanceEdge.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftBalanceEdge.graphql.swift; sourceTree = ""; }; + 09BE4B220A4F8465C3D01BA5 /* NetworkFee.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NetworkFee.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NetworkFee.graphql.swift; sourceTree = ""; }; + 0B0505DB30407C221B6A8491 /* TokenProjectMarket.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenProjectMarket.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/TokenProjectMarket.graphql.swift; sourceTree = ""; }; 0B7E5D62E11408EB5F0F5A80 /* Pods-WidgetsCore.beta.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetsCore.beta.xcconfig"; path = "Target Support Files/Pods-WidgetsCore/Pods-WidgetsCore.beta.xcconfig"; sourceTree = ""; }; - 0BB4A40E30E1D985519DB3CF /* TokenProtectionInfoParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenProtectionInfoParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TokenProtectionInfoParts.graphql.swift; sourceTree = ""; }; 0C19DE44A750FB17647FF2B6 /* Pods-Widgets.beta.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Widgets.beta.xcconfig"; path = "Target Support Files/Pods-Widgets/Pods-Widgets.beta.xcconfig"; sourceTree = ""; }; + 0D09FFE2B5E56CDCAB2F9455 /* NftCollectionConnection.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftCollectionConnection.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftCollectionConnection.graphql.swift; sourceTree = ""; }; 0DB282242CDADB260014CF77 /* EmbeddedWallet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = EmbeddedWallet.m; sourceTree = ""; }; 0DB282252CDADB260014CF77 /* EmbeddedWallet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmbeddedWallet.swift; sourceTree = ""; }; 0DC6ADEF2B1E2C0F0092909C /* PortfolioValueModifier.graphql.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortfolioValueModifier.graphql.swift; sourceTree = ""; }; @@ -574,91 +580,80 @@ 0DE251442C13B69D005F47F9 /* OnRampTransfer.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnRampTransfer.graphql.swift; sourceTree = ""; }; 0DE251452C13B69D005F47F9 /* OnRampServiceProvider.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnRampServiceProvider.graphql.swift; sourceTree = ""; }; 0DE251462C13B69D005F47F9 /* OnRampTransactionDetails.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnRampTransactionDetails.graphql.swift; sourceTree = ""; }; + 0E7FF51A507BF788E64E3EFE /* HomeScreenTokensQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = HomeScreenTokensQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/HomeScreenTokensQuery.graphql.swift; sourceTree = ""; }; + 1053483279043EED7612BE90 /* HistoryDuration.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = HistoryDuration.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/HistoryDuration.graphql.swift; sourceTree = ""; }; 1064E23E366D0C2C2B20C30E /* Pods_WidgetIntentExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_WidgetIntentExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 1166529407CA13E4D4F24F12 /* TokenFeeDataParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenFeeDataParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TokenFeeDataParts.graphql.swift; sourceTree = ""; }; 1193B3A845BC3BE8CAA00D01 /* Pods-OneSignalNotificationServiceExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OneSignalNotificationServiceExtension.release.xcconfig"; path = "Target Support Files/Pods-OneSignalNotificationServiceExtension/Pods-OneSignalNotificationServiceExtension.release.xcconfig"; sourceTree = ""; }; - 12D3AB6C2FC9513E9F224B7C /* OnRampTransactionDetails.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = OnRampTransactionDetails.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/OnRampTransactionDetails.graphql.swift; sourceTree = ""; }; - 137696908D703632CEE3AB28 /* TokenBalanceMainParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenBalanceMainParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TokenBalanceMainParts.graphql.swift; sourceTree = ""; }; + 1276D33B06E840B8E6839F39 /* TokenProjectDescriptionQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenProjectDescriptionQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/TokenProjectDescriptionQuery.graphql.swift; sourceTree = ""; }; 13B07F961A680F5B00A75B9A /* Uniswap.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Uniswap.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = Uniswap/AppDelegate.h; sourceTree = ""; }; 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Uniswap/Images.xcassets; sourceTree = ""; }; 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Uniswap/Info.plist; sourceTree = ""; }; - 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = Uniswap/main.m; sourceTree = ""; }; - 148617FD73F2CD7117960DBA /* SwapOrderType.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SwapOrderType.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/SwapOrderType.graphql.swift; sourceTree = ""; }; 15092E550A1C78508ABA3280 /* Pods_OneSignalNotificationServiceExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_OneSignalNotificationServiceExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 17819A49DE69723EC60D9D72 /* FeeData.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FeeData.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/FeeData.graphql.swift; sourceTree = ""; }; + 166A9AE7203545C397E683E8 /* OffRampTransfer.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = OffRampTransfer.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/OffRampTransfer.graphql.swift; sourceTree = ""; }; 178644A78AB62609EFDB66B3 /* Pods-Uniswap.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Uniswap.release.xcconfig"; path = "Target Support Files/Pods-Uniswap/Pods-Uniswap.release.xcconfig"; sourceTree = ""; }; + 1810C60B56CC74150C868801 /* NftBalancesFilterInput.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftBalancesFilterInput.graphql.swift; path = WidgetsCore/MobileSchema/Schema/InputObjects/NftBalancesFilterInput.graphql.swift; sourceTree = ""; }; + 181209B4CDE80BDC80891CFD /* TokenBalance.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenBalance.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/TokenBalance.graphql.swift; sourceTree = ""; }; 1834199AFFB04D91B05FFB64 /* InputMono-Regular.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "InputMono-Regular.ttf"; path = "../src/assets/fonts/InputMono-Regular.ttf"; sourceTree = ""; }; - 1849039FD4A210DA990363C8 /* Portfolio.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Portfolio.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/Portfolio.graphql.swift; sourceTree = ""; }; - 18B9EB43501127BE1B14F1A5 /* NftContract.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftContract.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftContract.graphql.swift; sourceTree = ""; }; + 18D48B25E03122149B0D1304 /* TransactionType.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TransactionType.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/TransactionType.graphql.swift; sourceTree = ""; }; + 190523DBBA423868C74FF80D /* TokenProjectUrlsParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenProjectUrlsParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TokenProjectUrlsParts.graphql.swift; sourceTree = ""; }; 1CC6ADAADCA38FDAEB181E86 /* Pods-WidgetIntentExtension.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetIntentExtension.dev.xcconfig"; path = "Target Support Files/Pods-WidgetIntentExtension/Pods-WidgetIntentExtension.dev.xcconfig"; sourceTree = ""; }; - 1FC7B80EC0E0D2134B67575B /* NftBalanceConnection.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftBalanceConnection.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftBalanceConnection.graphql.swift; sourceTree = ""; }; - 207F4DF7664454C31DE069F8 /* NFTItemScreenQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NFTItemScreenQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/NFTItemScreenQuery.graphql.swift; sourceTree = ""; }; - 2139ABF56EA067ED792A48C9 /* OnRampTransfer.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = OnRampTransfer.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/OnRampTransfer.graphql.swift; sourceTree = ""; }; - 218B8AB05C80D919671D3970 /* NftAssetConnection.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftAssetConnection.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftAssetConnection.graphql.swift; sourceTree = ""; }; + 1CDAC035887C3908C3BD1A77 /* NftActivityType.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftActivityType.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/NftActivityType.graphql.swift; sourceTree = ""; }; + 1D9189C818AFF8E988F861D0 /* IContract.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = IContract.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Interfaces/IContract.graphql.swift; sourceTree = ""; }; + 1F1731869775D16A9EAB475C /* NftAssetTraitInput.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftAssetTraitInput.graphql.swift; path = WidgetsCore/MobileSchema/Schema/InputObjects/NftAssetTraitInput.graphql.swift; sourceTree = ""; }; 2226DF79BEAFECEE11A51347 /* Pods_Uniswap.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Uniswap.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 224CA82F1871016F4173F69E /* TransactionListQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TransactionListQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/TransactionListQuery.graphql.swift; sourceTree = ""; }; - 236126979349098B98C70F27 /* TokenProjectUrlsParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenProjectUrlsParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TokenProjectUrlsParts.graphql.swift; sourceTree = ""; }; - 2370BAD675DC8064C00AC037 /* TokenBalanceQuantityParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenBalanceQuantityParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TokenBalanceQuantityParts.graphql.swift; sourceTree = ""; }; - 25B4C7C7FEF32EAF10D030F9 /* Currency.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Currency.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/Currency.graphql.swift; sourceTree = ""; }; - 27F8B915042BD8D67D9627EA /* HomeScreenTokenParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = HomeScreenTokenParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/HomeScreenTokenParts.graphql.swift; sourceTree = ""; }; - 2ABA47847A73130D356EB6BF /* TransactionStatus.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TransactionStatus.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/TransactionStatus.graphql.swift; sourceTree = ""; }; - 2BA0ACE5B9B2C41187CD41F3 /* FeedTransactionListQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FeedTransactionListQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/FeedTransactionListQuery.graphql.swift; sourceTree = ""; }; - 2C2F4BE3080D4129D4478528 /* TokenBasicProjectParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenBasicProjectParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TokenBasicProjectParts.graphql.swift; sourceTree = ""; }; - 2D95FE36D2667E070345CDD0 /* NftActivityType.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftActivityType.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/NftActivityType.graphql.swift; sourceTree = ""; }; - 2E49E724432C70A2551FB2C7 /* SchemaConfiguration.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SchemaConfiguration.swift; path = WidgetsCore/MobileSchema/Schema/SchemaConfiguration.swift; sourceTree = ""; }; + 22F8093091A9E5FC0D6284BA /* SafetyLevel.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SafetyLevel.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/SafetyLevel.graphql.swift; sourceTree = ""; }; + 243576FAC8D2801F9D99ECC9 /* TopTokensQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TopTokensQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/TopTokensQuery.graphql.swift; sourceTree = ""; }; + 2E8286984864AD69A0FAE3BC /* TokenBasicProjectParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenBasicProjectParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TokenBasicProjectParts.graphql.swift; sourceTree = ""; }; 2E8B7D36D2E14D9488F351EB /* Pods_Widgets.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Widgets.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 315B8A905FA2C60C053F138B /* OnRampTransactionsAuth.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = OnRampTransactionsAuth.graphql.swift; path = WidgetsCore/MobileSchema/Schema/InputObjects/OnRampTransactionsAuth.graphql.swift; sourceTree = ""; }; - 31ED613D63FE9C56D9270517 /* Image.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Image.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/Image.graphql.swift; sourceTree = ""; }; - 32012AC135EBD991C8F02F92 /* TokenSortableField.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenSortableField.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/TokenSortableField.graphql.swift; sourceTree = ""; }; - 3377F7BB38BD5A45AE31909D /* TokenBasicInfoParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenBasicInfoParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TokenBasicInfoParts.graphql.swift; sourceTree = ""; }; - 37038CCEB11E70E2027EF4EA /* NftCollectionEdge.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftCollectionEdge.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftCollectionEdge.graphql.swift; sourceTree = ""; }; - 396B6AB3BB41898B56EB3828 /* OffRampTransactionDetails.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = OffRampTransactionDetails.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/OffRampTransactionDetails.graphql.swift; sourceTree = ""; }; - 39F60B2B54796DC978765C99 /* AssetChange.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AssetChange.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Unions/AssetChange.graphql.swift; sourceTree = ""; }; - 39FC2A13550FA6754EC1A7FC /* NftOrderConnection.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftOrderConnection.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftOrderConnection.graphql.swift; sourceTree = ""; }; + 2FB1590D18A58D37C750D9AC /* Token.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Token.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/Token.graphql.swift; sourceTree = ""; }; + 310373063D8A11E9E02314FE /* NftAssetsFilterInput.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftAssetsFilterInput.graphql.swift; path = WidgetsCore/MobileSchema/Schema/InputObjects/NftAssetsFilterInput.graphql.swift; sourceTree = ""; }; + 31CBA4397C44A04E218AEF28 /* TimestampedAmount.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TimestampedAmount.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/TimestampedAmount.graphql.swift; sourceTree = ""; }; + 37376E4CF3A4B76E9DAE150C /* IAmount.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = IAmount.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Interfaces/IAmount.graphql.swift; sourceTree = ""; }; 3A2186B1FF7FB85663D96EA9 /* Pods-OneSignalNotificationServiceExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OneSignalNotificationServiceExtension.debug.xcconfig"; path = "Target Support Files/Pods-OneSignalNotificationServiceExtension/Pods-OneSignalNotificationServiceExtension.debug.xcconfig"; sourceTree = ""; }; - 3B9A505509D9A85BE1DB7D05 /* TokenProjectMarket.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenProjectMarket.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/TokenProjectMarket.graphql.swift; sourceTree = ""; }; - 3BF42F88002431BFDC9FCA9F /* HistoryDuration.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = HistoryDuration.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/HistoryDuration.graphql.swift; sourceTree = ""; }; + 3B03A0A6AB0957A512A2D225 /* FavoriteTokenCardQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FavoriteTokenCardQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/FavoriteTokenCardQuery.graphql.swift; sourceTree = ""; }; 3C606D2C81014A0A8898F38E /* Basel-Grotesk-Medium.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Basel-Grotesk-Medium.otf"; path = "../src/assets/fonts/Basel-Grotesk-Medium.otf"; sourceTree = ""; }; 3D8FCE4CD401350CA74DCC89 /* Pods-WidgetsCoreTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetsCoreTests.debug.xcconfig"; path = "Target Support Files/Pods-WidgetsCoreTests/Pods-WidgetsCoreTests.debug.xcconfig"; sourceTree = ""; }; 3E279F675B02CBC50D3B57D5 /* Pods-WidgetsCore.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetsCore.release.xcconfig"; path = "Target Support Files/Pods-WidgetsCore/Pods-WidgetsCore.release.xcconfig"; sourceTree = ""; }; - 3F0EEAC1DADE827AE38EB8A6 /* FavoriteTokenCardQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FavoriteTokenCardQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/FavoriteTokenCardQuery.graphql.swift; sourceTree = ""; }; - 40E034582FBD2C374CEF808E /* ProtectionAttackType.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ProtectionAttackType.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/ProtectionAttackType.graphql.swift; sourceTree = ""; }; - 41003491EF939043CF9D0F4E /* PortfolioValueModifier.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PortfolioValueModifier.graphql.swift; path = WidgetsCore/MobileSchema/Schema/InputObjects/PortfolioValueModifier.graphql.swift; sourceTree = ""; }; - 41BA2129FFE961C8549C8A30 /* NftBalancesFilterInput.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftBalancesFilterInput.graphql.swift; path = WidgetsCore/MobileSchema/Schema/InputObjects/NftBalancesFilterInput.graphql.swift; sourceTree = ""; }; - 43E2ECCEFBC3BE8D13BE6CAB /* NftActivityFilterInput.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftActivityFilterInput.graphql.swift; path = WidgetsCore/MobileSchema/Schema/InputObjects/NftActivityFilterInput.graphql.swift; sourceTree = ""; }; - 4429CA6DA254ECC24A65DA14 /* MultiplePortfolioBalancesQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MultiplePortfolioBalancesQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/MultiplePortfolioBalancesQuery.graphql.swift; sourceTree = ""; }; + 3E5D1A04B42B25218C53C107 /* OnRampTransfer.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = OnRampTransfer.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/OnRampTransfer.graphql.swift; sourceTree = ""; }; + 3EF9AEC00868BFD7CEBF202C /* BlockaidFees.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BlockaidFees.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/BlockaidFees.graphql.swift; sourceTree = ""; }; + 45FFF7DE2E8C2A6400362570 /* SilentPushEventEmitter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SilentPushEventEmitter.m; sourceTree = ""; }; + 45FFF7E02E8C2E6100362570 /* SilentPushEventEmitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SilentPushEventEmitter.swift; sourceTree = ""; }; 4781CD4CDD95B5792B793F75 /* Pods-Uniswap-UniswapTests.beta.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Uniswap-UniswapTests.beta.xcconfig"; path = "Target Support Files/Pods-Uniswap-UniswapTests/Pods-Uniswap-UniswapTests.beta.xcconfig"; sourceTree = ""; }; - 4981A14906A35E8A0B7F9457 /* TokenProject.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenProject.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/TokenProject.graphql.swift; sourceTree = ""; }; - 49BD6C776029FAF4FC711DE7 /* NftOrderEdge.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftOrderEdge.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftOrderEdge.graphql.swift; sourceTree = ""; }; - 4A8990A17F489F284E0DB1B3 /* Chain.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Chain.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/Chain.graphql.swift; sourceTree = ""; }; - 4ACFA5B4204741038C986C5D /* SafetyLevel.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SafetyLevel.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/SafetyLevel.graphql.swift; sourceTree = ""; }; - 4B308EC3DB7C3C1DD5DDE03D /* NftStandard.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftStandard.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/NftStandard.graphql.swift; sourceTree = ""; }; 4C445DB9798210862C34D0E0 /* Pods-WidgetsCoreTests.beta.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetsCoreTests.beta.xcconfig"; path = "Target Support Files/Pods-WidgetsCoreTests/Pods-WidgetsCoreTests.beta.xcconfig"; sourceTree = ""; }; - 4DB4379B5E222207CA7328D0 /* NftActivityEdge.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftActivityEdge.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftActivityEdge.graphql.swift; sourceTree = ""; }; + 4DD55DFFC717D37DD960B82E /* NftBalanceEdge.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftBalanceEdge.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftBalanceEdge.graphql.swift; sourceTree = ""; }; 4DF5F26A06553EFDD4D99214 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-Uniswap/ExpoModulesProvider.swift"; sourceTree = ""; }; - 4E32BBB65A6DD9CFF608C8E8 /* NftApproval.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftApproval.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftApproval.graphql.swift; sourceTree = ""; }; - 4E5FA43C5E31035775917C4E /* NftBalanceAssetInput.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftBalanceAssetInput.graphql.swift; path = WidgetsCore/MobileSchema/Schema/InputObjects/NftBalanceAssetInput.graphql.swift; sourceTree = ""; }; - 4F4F47D22B749A438B5BF674 /* NftCollectionMarket.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftCollectionMarket.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftCollectionMarket.graphql.swift; sourceTree = ""; }; - 54B1C71AED738D17898103A6 /* Dimensions.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Dimensions.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/Dimensions.graphql.swift; sourceTree = ""; }; - 556339705BD103365D4ED07B /* NftAssetTraitInput.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftAssetTraitInput.graphql.swift; path = WidgetsCore/MobileSchema/Schema/InputObjects/NftAssetTraitInput.graphql.swift; sourceTree = ""; }; - 5640432978873FB030467750 /* TopTokenParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TopTokenParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TopTokenParts.graphql.swift; sourceTree = ""; }; + 50C3E075E850CEC1714BE52C /* NftActivityFilterInput.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftActivityFilterInput.graphql.swift; path = WidgetsCore/MobileSchema/Schema/InputObjects/NftActivityFilterInput.graphql.swift; sourceTree = ""; }; + 51F133663B435829DE47E3CE /* TokenSortableField.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenSortableField.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/TokenSortableField.graphql.swift; sourceTree = ""; }; + 522135E54BDB258B45B5A694 /* SchemaMetadata.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SchemaMetadata.graphql.swift; path = WidgetsCore/MobileSchema/Schema/SchemaMetadata.graphql.swift; sourceTree = ""; }; + 5397E77A577E9952D2477020 /* Portfolio.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Portfolio.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/Portfolio.graphql.swift; sourceTree = ""; }; + 53F4E5E9341BE66984935185 /* SwapOrderDetails.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SwapOrderDetails.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/SwapOrderDetails.graphql.swift; sourceTree = ""; }; + 55081ADF188C967FC1ECD289 /* TokenMarketParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenMarketParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TokenMarketParts.graphql.swift; sourceTree = ""; }; + 55240C84512D3851C8B9F027 /* TransactionDetails.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TransactionDetails.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/TransactionDetails.graphql.swift; sourceTree = ""; }; + 559CBC7F568C769C48E7C6F4 /* TransactionHistoryUpdaterQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TransactionHistoryUpdaterQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/TransactionHistoryUpdaterQuery.graphql.swift; sourceTree = ""; }; + 55A243177E9FA92A0524A222 /* NftContract.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftContract.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftContract.graphql.swift; sourceTree = ""; }; + 55C67E53A2C8AEB6AE92E33F /* NftActivity.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftActivity.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftActivity.graphql.swift; sourceTree = ""; }; 56FE9C9AF785221B7E3F4C04 /* Pods-Uniswap.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Uniswap.dev.xcconfig"; path = "Target Support Files/Pods-Uniswap/Pods-Uniswap.dev.xcconfig"; sourceTree = ""; }; + 595699514939A2CE780AF0CF /* MultiplePortfolioBalancesQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MultiplePortfolioBalancesQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/MultiplePortfolioBalancesQuery.graphql.swift; sourceTree = ""; }; + 5956ECBE73216D266D8D2E86 /* Query.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Query.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/Query.graphql.swift; sourceTree = ""; }; + 59DD4D834D7B42A6D02F4F12 /* ApplicationContract.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ApplicationContract.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/ApplicationContract.graphql.swift; sourceTree = ""; }; + 5B0A1F5F6FC3E1141E228B7D /* TokenBalanceQuantityParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenBalanceQuantityParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TokenBalanceQuantityParts.graphql.swift; sourceTree = ""; }; 5B4398E82DD3B22C00F6BE08 /* PrivateKeyDisplayManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PrivateKeyDisplayManager.m; sourceTree = ""; }; 5B4398E92DD3B22C00F6BE08 /* PrivateKeyDisplayManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateKeyDisplayManager.swift; sourceTree = ""; }; 5B4398EA2DD3B22C00F6BE08 /* PrivateKeyDisplayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateKeyDisplayView.swift; sourceTree = ""; }; 5B4CEC5E2DD65DD4009F082B /* CopyIconOutline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyIconOutline.swift; sourceTree = ""; }; - 5E0D68CC8D1F13D864F069BB /* IContract.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = IContract.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Interfaces/IContract.graphql.swift; sourceTree = ""; }; + 5BED80DB034487FCA1450EDB /* OnRampServiceProvider.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = OnRampServiceProvider.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/OnRampServiceProvider.graphql.swift; sourceTree = ""; }; + 5C9F3B31FC6AF8CE30CD7DA2 /* NftAsset.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftAsset.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftAsset.graphql.swift; sourceTree = ""; }; 5E5E0A622D380F5700E166AA /* Env.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Env.swift; sourceTree = ""; }; 5EFB78352B1E585000E77EAC /* ConvertQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConvertQuery.graphql.swift; sourceTree = ""; }; - 600250B00B6C02AAB063818B /* NftMarketplace.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftMarketplace.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/NftMarketplace.graphql.swift; sourceTree = ""; }; - 608B07304ED1387B5AEAA3BB /* ApplicationContract.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ApplicationContract.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/ApplicationContract.graphql.swift; sourceTree = ""; }; 62CEA9F2D5176D20A6402A3E /* Pods-Uniswap.beta.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Uniswap.beta.xcconfig"; path = "Target Support Files/Pods-Uniswap/Pods-Uniswap.beta.xcconfig"; sourceTree = ""; }; - 63ACFF2A1D5AE3498731AC69 /* NftCollection.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftCollection.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftCollection.graphql.swift; sourceTree = ""; }; - 647933A5DC35BDFEEF3620A1 /* Token.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Token.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/Token.graphql.swift; sourceTree = ""; }; + 62DDC1F42E7172783EAF3776 /* NftBalanceConnection.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftBalanceConnection.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftBalanceConnection.graphql.swift; sourceTree = ""; }; 649A7A762D9AE70B00B53589 /* KeychainConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainConstants.swift; sourceTree = ""; }; 649A7A772D9AE70B00B53589 /* KeychainUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainUtils.swift; sourceTree = ""; }; - 69F2FFD11FB5E462CC454A15 /* TransactionType.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TransactionType.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/TransactionType.graphql.swift; sourceTree = ""; }; - 6A84A192F750A0F1BC8D7A69 /* OnRampServiceProvider.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = OnRampServiceProvider.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/OnRampServiceProvider.graphql.swift; sourceTree = ""; }; + 64D494D944670E557175A694 /* TransactionDirection.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TransactionDirection.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/TransactionDirection.graphql.swift; sourceTree = ""; }; + 65AD13BFAD4EBA18AA6205E3 /* TokenBalanceParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenBalanceParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TokenBalanceParts.graphql.swift; sourceTree = ""; }; + 67C0D95F7EA9E7ACA9B18692 /* NftOrder.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftOrder.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftOrder.graphql.swift; sourceTree = ""; }; + 696F6C5220D0072E380E198F /* NftBalanceAssetInput.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftBalanceAssetInput.graphql.swift; path = WidgetsCore/MobileSchema/Schema/InputObjects/NftBalanceAssetInput.graphql.swift; sourceTree = ""; }; 6BC7D07B2B5FF02400617C95 /* ScantasticEncryption.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ScantasticEncryption.m; sourceTree = ""; }; 6BC7D07C2B5FF02400617C95 /* ScantasticEncryption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScantasticEncryption.swift; sourceTree = ""; }; 6BC7D07D2B5FF02400617C95 /* EncryptionUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EncryptionUtils.swift; sourceTree = ""; }; @@ -669,26 +664,28 @@ 6CA91BDE2A95226200C4063E /* RNCloudStorageBackupsManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNCloudStorageBackupsManager.m; sourceTree = ""; }; 6CA91BDF2A95226200C4063E /* EncryptionHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EncryptionHelper.swift; sourceTree = ""; }; 6CA91BE02A95226200C4063E /* RNCloudStorageBackupsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RNCloudStorageBackupsManager.swift; sourceTree = ""; }; - 6DF7C8888BE6AFA2F22889FE /* NftAssetEdge.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftAssetEdge.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftAssetEdge.graphql.swift; sourceTree = ""; }; - 6E3A78386B3B779B688021FF /* NftOrder.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftOrder.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftOrder.graphql.swift; sourceTree = ""; }; + 6D4C849A7805FCCEE8C8C3E6 /* AssetChange.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AssetChange.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Unions/AssetChange.graphql.swift; sourceTree = ""; }; + 6E4DEE7FC8D7BCD030CFA4D3 /* ConvertQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConvertQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/ConvertQuery.graphql.swift; sourceTree = ""; }; + 6F0A91F56B1CA2E71B479BAD /* WidgetTokensQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = WidgetTokensQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/WidgetTokensQuery.graphql.swift; sourceTree = ""; }; 6F33E8069B7B40AFB313B8B0 /* Basel-Grotesk-Book.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Basel-Grotesk-Book.otf"; path = "../src/assets/fonts/Basel-Grotesk-Book.otf"; sourceTree = ""; }; 6F3DC921A65D749C0852B10C /* Pods-Uniswap-UniswapTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Uniswap-UniswapTests.debug.xcconfig"; path = "Target Support Files/Pods-Uniswap-UniswapTests/Pods-Uniswap-UniswapTests.debug.xcconfig"; sourceTree = ""; }; 6F7814C6D40D9C348EA1F1C7 /* Pods-OneSignalNotificationServiceExtension.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OneSignalNotificationServiceExtension.dev.xcconfig"; path = "Target Support Files/Pods-OneSignalNotificationServiceExtension/Pods-OneSignalNotificationServiceExtension.dev.xcconfig"; sourceTree = ""; }; - 6FF8B660E9C2C0D8DDF7588A /* TokenProjectDescriptionQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenProjectDescriptionQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/TokenProjectDescriptionQuery.graphql.swift; sourceTree = ""; }; - 70851E3D1A4B296A1CE22A20 /* NftAssetTrait.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftAssetTrait.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftAssetTrait.graphql.swift; sourceTree = ""; }; - 730CA356D6E1D498CC0C1472 /* NftsTabQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftsTabQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/NftsTabQuery.graphql.swift; sourceTree = ""; }; - 74885865ED8CFEA875E40916 /* TokenQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/TokenQuery.graphql.swift; sourceTree = ""; }; + 74AD16E33DC518A42C386FE2 /* NftTransfer.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftTransfer.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftTransfer.graphql.swift; sourceTree = ""; }; + 764C4757EB194D13A8857DFA /* Dimensions.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Dimensions.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/Dimensions.graphql.swift; sourceTree = ""; }; 7A7637BBC9B3A68E0338D96E /* Pods-WidgetIntentExtension.beta.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetIntentExtension.beta.xcconfig"; path = "Target Support Files/Pods-WidgetIntentExtension/Pods-WidgetIntentExtension.beta.xcconfig"; sourceTree = ""; }; - 7BD3F0794908635CDFA7DAB6 /* ConvertQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConvertQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/ConvertQuery.graphql.swift; sourceTree = ""; }; - 7E1F288B7EDDE906C4C00C46 /* DescriptionTranslations.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DescriptionTranslations.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/DescriptionTranslations.graphql.swift; sourceTree = ""; }; + 7AFDF569E43F173C2F5A07AF /* SwapOrderType.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SwapOrderType.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/SwapOrderType.graphql.swift; sourceTree = ""; }; + 7DBCFBC330BF97BA2F630908 /* Chain.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Chain.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/Chain.graphql.swift; sourceTree = ""; }; 82C9871585F60F92D079FB95 /* Pods-Widgets.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Widgets.release.xcconfig"; path = "Target Support Files/Pods-Widgets/Pods-Widgets.release.xcconfig"; sourceTree = ""; }; - 84997376A0B436ECC0A996C5 /* ExploreSearchQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExploreSearchQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/ExploreSearchQuery.graphql.swift; sourceTree = ""; }; + 835D9DF76533D22EDF0E9D67 /* TokenProtectionInfoParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenProtectionInfoParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TokenProtectionInfoParts.graphql.swift; sourceTree = ""; }; + 83FB27D93A9468E6DB47231C /* NftMarketplace.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftMarketplace.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/NftMarketplace.graphql.swift; sourceTree = ""; }; + 85B43D489CC5ACF1EA34E9EB /* NftAssetTrait.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftAssetTrait.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftAssetTrait.graphql.swift; sourceTree = ""; }; 8719E5872CC41AB64503E903 /* Pods-WidgetIntentExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetIntentExtension.debug.xcconfig"; path = "Target Support Files/Pods-WidgetIntentExtension/Pods-WidgetIntentExtension.debug.xcconfig"; sourceTree = ""; }; - 8A4EB7BAD2C90E61FD6C30AF /* ActivityDetails.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ActivityDetails.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Unions/ActivityDetails.graphql.swift; sourceTree = ""; }; - 8A5342DEF1410E0E81E97F40 /* SearchTokensQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SearchTokensQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/SearchTokensQuery.graphql.swift; sourceTree = ""; }; - 8B477EBEA1EDA3914C77E8A6 /* NftAssetsFilterInput.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftAssetsFilterInput.graphql.swift; path = WidgetsCore/MobileSchema/Schema/InputObjects/NftAssetsFilterInput.graphql.swift; sourceTree = ""; }; - 8BEADEB4AE05B860CF2232D1 /* NetworkFee.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NetworkFee.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NetworkFee.graphql.swift; sourceTree = ""; }; - 8DA4E7C052A6C4AC3A9DCDA6 /* TokenTransfer.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenTransfer.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/TokenTransfer.graphql.swift; sourceTree = ""; }; + 87442A651C068E552BEA89C6 /* MobileSchema.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MobileSchema.graphql.swift; path = WidgetsCore/MobileSchema/MobileSchema.graphql.swift; sourceTree = ""; }; + 87CF9B03AE29A19C5B0F4E35 /* OnRampTransactionDetails.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = OnRampTransactionDetails.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/OnRampTransactionDetails.graphql.swift; sourceTree = ""; }; + 8856DFFC7FDC9BF105B0B4E3 /* TokenProject.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenProject.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/TokenProject.graphql.swift; sourceTree = ""; }; + 8B2A92162EB3E78E00990413 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = Uniswap/AppDelegate.swift; sourceTree = ""; }; + 8B2A92182EB3E79500990413 /* Uniswap-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "Uniswap-Bridging-Header.h"; path = "Uniswap/Uniswap-Bridging-Header.h"; sourceTree = ""; }; + 8C9047B981FA322A727947F7 /* Image.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Image.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/Image.graphql.swift; sourceTree = ""; }; 8E89C3A62AB8AAA400C84DE5 /* MnemonicConfirmationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MnemonicConfirmationView.swift; sourceTree = ""; }; 8E89C3A72AB8AAA400C84DE5 /* MnemonicDisplayView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MnemonicDisplayView.swift; sourceTree = ""; }; 8E89C3A92AB8AAA400C84DE5 /* MnemonicConfirmationWordBankView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MnemonicConfirmationWordBankView.swift; sourceTree = ""; }; @@ -719,11 +716,14 @@ 91D5016E2CDBEAE700B09B7F /* TopTokenParts.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopTokenParts.graphql.swift; sourceTree = ""; }; 91D5016F2CDBEAE700B09B7F /* TokenFeeDataParts.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenFeeDataParts.graphql.swift; sourceTree = ""; }; 91D5017C2CDBEAF600B09B7F /* HomeScreenTokenParts.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeScreenTokenParts.graphql.swift; sourceTree = ""; }; - 92DE390311705FF1EE65C0E6 /* NftCollectionConnection.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftCollectionConnection.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftCollectionConnection.graphql.swift; sourceTree = ""; }; - 985DDC8324B872DD44E87781 /* PortfolioBalancesQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PortfolioBalancesQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/PortfolioBalancesQuery.graphql.swift; sourceTree = ""; }; - 98F596250F55A3D6E9E1F499 /* TokenParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TokenParts.graphql.swift; sourceTree = ""; }; - 9DFBD29C0BDFDB4464B8189C /* TokenPriceHistoryQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenPriceHistoryQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/TokenPriceHistoryQuery.graphql.swift; sourceTree = ""; }; - 9E975BEDBED5FE51D0DC6E96 /* TokenApproval.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenApproval.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/TokenApproval.graphql.swift; sourceTree = ""; }; + 945EF0A3BD6F19DA14E40AE1 /* TransactionStatus.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TransactionStatus.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/TransactionStatus.graphql.swift; sourceTree = ""; }; + 95283AC560844B50E95934E4 /* TransactionListQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TransactionListQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/TransactionListQuery.graphql.swift; sourceTree = ""; }; + 957A35D2DE7A140BA198A0F1 /* TokenDetailsScreenQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenDetailsScreenQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/TokenDetailsScreenQuery.graphql.swift; sourceTree = ""; }; + 9602E2A290179F8741EDD35A /* TokenPriceHistoryQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenPriceHistoryQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/TokenPriceHistoryQuery.graphql.swift; sourceTree = ""; }; + 97FBBE677B56FFE07052EF1D /* PageInfo.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PageInfo.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/PageInfo.graphql.swift; sourceTree = ""; }; + 98F489175099FE697721272D /* NftOrderConnection.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftOrderConnection.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftOrderConnection.graphql.swift; sourceTree = ""; }; + 9BEBBC668E69EE3A1FC6AB05 /* TokenTransfer.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenTransfer.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/TokenTransfer.graphql.swift; sourceTree = ""; }; + 9EFADDBB38BB9261933C1349 /* FeedTransactionListQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FeedTransactionListQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/FeedTransactionListQuery.graphql.swift; sourceTree = ""; }; 9F00A4392B33894C0088A0D0 /* ApplicationContract.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApplicationContract.graphql.swift; sourceTree = ""; }; 9F29D4EC2B47126D004D003A /* NftBalanceAssetInput.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftBalanceAssetInput.graphql.swift; sourceTree = ""; }; 9F3500812A8AA5890077BFC5 /* EXSplashScreen.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = EXSplashScreen.xcframework; path = "../../../node_modules/expo-splash-screen/ios/EXSplashScreen.xcframework"; sourceTree = ""; }; @@ -737,74 +737,75 @@ 9FCEBEFF2A95A8E00079EDDB /* RNWalletConnect.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RNWalletConnect.swift; path = Uniswap/WalletConnect/RNWalletConnect.swift; sourceTree = ""; }; 9FCEBF022A95A99B0079EDDB /* RCTThemeModule.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RCTThemeModule.h; path = Appearance/RCTThemeModule.h; sourceTree = ""; }; 9FCEBF032A95A99B0079EDDB /* RCTThemeModule.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RCTThemeModule.m; path = Appearance/RCTThemeModule.m; sourceTree = ""; }; - 9FEC9B8A2A858CF1003CD019 /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = Uniswap/AppDelegate.m; sourceTree = ""; }; A32F9FBC272343C8002CFCDB /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; A3F0A5B0272B1DFA00895B25 /* KeychainSwiftDistrib.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainSwiftDistrib.swift; sourceTree = ""; }; - A4E0C448C5D45BF1071FA458 /* MobileSchema.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MobileSchema.graphql.swift; path = WidgetsCore/MobileSchema/MobileSchema.graphql.swift; sourceTree = ""; }; A70E4DD32C25DA0A002D6D86 /* NetworkFee.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NetworkFee.graphql.swift; path = MobileSchema/Schema/Objects/NetworkFee.graphql.swift; sourceTree = ""; }; A70E4DD52C260416002D6D86 /* SwapOrderType.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SwapOrderType.graphql.swift; path = MobileSchema/Schema/Enums/SwapOrderType.graphql.swift; sourceTree = ""; }; A70E4DD62C260416002D6D86 /* SwapOrderStatus.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SwapOrderStatus.graphql.swift; path = MobileSchema/Schema/Enums/SwapOrderStatus.graphql.swift; sourceTree = ""; }; - A7A33933670CCAD9E62A1A80 /* TokenDetailsScreenQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenDetailsScreenQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/TokenDetailsScreenQuery.graphql.swift; sourceTree = ""; }; A7B8EFCA2BF68F0D00CA4A1C /* FeeData.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FeeData.graphql.swift; path = MobileSchema/Schema/Objects/FeeData.graphql.swift; sourceTree = ""; }; A7C9F415D0E128A43003E071 /* Pods-Uniswap.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Uniswap.debug.xcconfig"; path = "Target Support Files/Pods-Uniswap/Pods-Uniswap.debug.xcconfig"; sourceTree = ""; }; - A9030AC59702756C39396FFE /* NftsQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftsQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/NftsQuery.graphql.swift; sourceTree = ""; }; - ABE51731EB853137302A7B0D /* TransactionDetails.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TransactionDetails.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/TransactionDetails.graphql.swift; sourceTree = ""; }; + A98CEFE7189D63880FD5A702 /* TokenProjectsQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenProjectsQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/TokenProjectsQuery.graphql.swift; sourceTree = ""; }; + AAB6B8C64F7525D0D0E978C6 /* ProtectionResult.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ProtectionResult.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/ProtectionResult.graphql.swift; sourceTree = ""; }; + AB772B265A35BFBC3A7761AD /* HomeScreenTokenParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = HomeScreenTokenParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/HomeScreenTokenParts.graphql.swift; sourceTree = ""; }; AC0EE0972BD826E700BCCF07 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = Uniswap/PrivacyInfo.xcprivacy; sourceTree = ""; }; AC2794442C51541E00F9AF68 /* sourcemaps-datadog.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "sourcemaps-datadog.sh"; sourceTree = ""; }; AC2EF4022C914B1600EEEFDB /* fonts */ = {isa = PBXFileReference; lastKnownFileType = folder; name = fonts; path = ../src/assets/fonts; sourceTree = ""; }; - AF3D7A923E5E1FB504E1F64D /* TransactionHistoryUpdaterQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TransactionHistoryUpdaterQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/TransactionHistoryUpdaterQuery.graphql.swift; sourceTree = ""; }; - B033206DC974BCB6AF8CC613 /* SelectWalletScreenQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SelectWalletScreenQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/SelectWalletScreenQuery.graphql.swift; sourceTree = ""; }; + AD452B34344D670BA23EA331 /* TokenBalanceMainParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenBalanceMainParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TokenBalanceMainParts.graphql.swift; sourceTree = ""; }; + AD86EFAA0D6B8FBECCC4828B /* NftAssetConnection.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftAssetConnection.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftAssetConnection.graphql.swift; sourceTree = ""; }; + AE2A9FBFB7F718246B668A60 /* NftProfile.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftProfile.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftProfile.graphql.swift; sourceTree = ""; }; + AED050A208757F137DBDA57D /* ContractInput.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ContractInput.graphql.swift; path = WidgetsCore/MobileSchema/Schema/InputObjects/ContractInput.graphql.swift; sourceTree = ""; }; B0DA4D39B1A6D74A1D05B99F /* Pods-WidgetsCore.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetsCore.debug.xcconfig"; path = "Target Support Files/Pods-WidgetsCore/Pods-WidgetsCore.debug.xcconfig"; sourceTree = ""; }; - B0DB67D6C438F623976727E9 /* TimestampedAmount.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TimestampedAmount.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/TimestampedAmount.graphql.swift; sourceTree = ""; }; - B3C3F0403FAEFDD632AC6CCE /* TokensQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokensQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/TokensQuery.graphql.swift; sourceTree = ""; }; - B7E03AA02B39A7A90C96AA76 /* HomeScreenTokensQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = HomeScreenTokensQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/HomeScreenTokensQuery.graphql.swift; sourceTree = ""; }; + B25620D6E0E297C56198F190 /* NftCollection.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftCollection.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftCollection.graphql.swift; sourceTree = ""; }; + B3CC3D9DE64A39897BA580B3 /* OnRampTransactionsAuth.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = OnRampTransactionsAuth.graphql.swift; path = WidgetsCore/MobileSchema/Schema/InputObjects/OnRampTransactionsAuth.graphql.swift; sourceTree = ""; }; + B3F4D5E8B9ED6E0FE29EA779 /* ProtectionAttackType.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ProtectionAttackType.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/ProtectionAttackType.graphql.swift; sourceTree = ""; }; + B5C2E70E43A02405C5CEAD7C /* TokenProjectMarketsParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenProjectMarketsParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TokenProjectMarketsParts.graphql.swift; sourceTree = ""; }; + BA3BCD49511DE84892B34ED0 /* NftsTabQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftsTabQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/NftsTabQuery.graphql.swift; sourceTree = ""; }; BA869E362D56B0B600D7A718 /* WidgetTokensQuery.graphql.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetTokensQuery.graphql.swift; sourceTree = ""; }; + BB76D1207400E308430A837F /* TokenParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TokenParts.graphql.swift; sourceTree = ""; }; BCB2A43E5FB0D7B69CA02312 /* Pods-WidgetsCore.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetsCore.dev.xcconfig"; path = "Target Support Files/Pods-WidgetsCore/Pods-WidgetsCore.dev.xcconfig"; sourceTree = ""; }; - BD0FC2D534CB82B151C7F9B7 /* ProtectionResult.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ProtectionResult.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/ProtectionResult.graphql.swift; sourceTree = ""; }; - BD35300F81746B2D4A23065A /* BlockaidFees.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BlockaidFees.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/BlockaidFees.graphql.swift; sourceTree = ""; }; BF9176E944C84910B1C0B057 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = Uniswap/SplashScreen.storyboard; sourceTree = ""; }; - BFC36D963A2A52D1E7CA77E4 /* TokenMarketParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenMarketParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TokenMarketParts.graphql.swift; sourceTree = ""; }; + BFFD4C1E5A4B30DE3CE02DB4 /* NftAssetEdge.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftAssetEdge.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftAssetEdge.graphql.swift; sourceTree = ""; }; + C0359DDEA2E2E17759FCC5CF /* SwapOrderStatus.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SwapOrderStatus.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/SwapOrderStatus.graphql.swift; sourceTree = ""; }; C26D739993D5C939C6FBB58A /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-Uniswap-UniswapTests/ExpoModulesProvider.swift"; sourceTree = ""; }; - C4790C2A43570CAC086B936E /* NftCollectionsFilterInput.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftCollectionsFilterInput.graphql.swift; path = WidgetsCore/MobileSchema/Schema/InputObjects/NftCollectionsFilterInput.graphql.swift; sourceTree = ""; }; - C65195A1CC9894C6341385C3 /* SchemaMetadata.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SchemaMetadata.graphql.swift; path = WidgetsCore/MobileSchema/Schema/SchemaMetadata.graphql.swift; sourceTree = ""; }; + C4CA32B2B2997D73A0185635 /* OffRampTransactionDetails.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = OffRampTransactionDetails.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/OffRampTransactionDetails.graphql.swift; sourceTree = ""; }; C89238E3ED9F3AC98876B573 /* Pods-WidgetsCoreTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetsCoreTests.release.xcconfig"; path = "Target Support Files/Pods-WidgetsCoreTests/Pods-WidgetsCoreTests.release.xcconfig"; sourceTree = ""; }; - C955CE2592785712A11892DE /* SwapOrderDetails.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SwapOrderDetails.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/SwapOrderDetails.graphql.swift; sourceTree = ""; }; - C9D5E6A07F404974045BD525 /* OffRampTransfer.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = OffRampTransfer.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/OffRampTransfer.graphql.swift; sourceTree = ""; }; + C8E5B09464C768590127BFA6 /* TokenMarket.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenMarket.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/TokenMarket.graphql.swift; sourceTree = ""; }; + CA3BFDC64C5E0A058877A348 /* NftCollectionScreenQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftCollectionScreenQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/NftCollectionScreenQuery.graphql.swift; sourceTree = ""; }; + CAC62CDEEBE5712219A457C7 /* BridgedWithdrawalInfo.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BridgedWithdrawalInfo.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/BridgedWithdrawalInfo.graphql.swift; sourceTree = ""; }; CB29AC0C0907A833F23D2C30 /* Pods_WidgetsCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_WidgetsCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - CC055C3EDFDE2966FCA83C9E /* TokenProjectMarketsParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenProjectMarketsParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TokenProjectMarketsParts.graphql.swift; sourceTree = ""; }; - CC2FBE9F353D3753FB94B8BF /* TransactionDirection.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TransactionDirection.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/TransactionDirection.graphql.swift; sourceTree = ""; }; - D20E9461A6C9C1CD6A24BEED /* Amount.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Amount.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/Amount.graphql.swift; sourceTree = ""; }; - D574AB5194929038A54B4D4B /* SwapOrderStatus.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SwapOrderStatus.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/SwapOrderStatus.graphql.swift; sourceTree = ""; }; + CBE0699A65BEEEE6E9A6B03F /* NftActivityConnection.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftActivityConnection.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftActivityConnection.graphql.swift; sourceTree = ""; }; + CD0CA37FB709C06F123EBF74 /* Currency.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Currency.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/Currency.graphql.swift; sourceTree = ""; }; + CF4115ACC8D8F0C7529AABA7 /* SchemaConfiguration.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SchemaConfiguration.swift; path = WidgetsCore/MobileSchema/Schema/SchemaConfiguration.swift; sourceTree = ""; }; + D13E592852F52B2855F05A5E /* TokenBasicInfoParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenBasicInfoParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TokenBasicInfoParts.graphql.swift; sourceTree = ""; }; + D34AC1057AD14362BB91E88B /* TokenStandard.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenStandard.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/TokenStandard.graphql.swift; sourceTree = ""; }; + D4C8174A3A9EB3244AA844F4 /* TokensQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokensQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/TokensQuery.graphql.swift; sourceTree = ""; }; D79B717BEAEA7857469D770A /* Pods-Widgets.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Widgets.debug.xcconfig"; path = "Target Support Files/Pods-Widgets/Pods-Widgets.debug.xcconfig"; sourceTree = ""; }; - DB35E518AD7FBFEBEA9CAB95 /* NftProfile.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftProfile.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftProfile.graphql.swift; sourceTree = ""; }; - DBA33683D2BA6BBE3251D67C /* WidgetTokensQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = WidgetTokensQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/WidgetTokensQuery.graphql.swift; sourceTree = ""; }; - E4624A41EC1468712BD77653 /* AssetActivity.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AssetActivity.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/AssetActivity.graphql.swift; sourceTree = ""; }; - E475EBABF7EA310435E2F19C /* PageInfo.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PageInfo.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/PageInfo.graphql.swift; sourceTree = ""; }; - E63EA0C75D8011BCC182492C /* AmountChange.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AmountChange.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/AmountChange.graphql.swift; sourceTree = ""; }; - E8A49D011C60137D4034CAC4 /* TokenProjectsQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenProjectsQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/TokenProjectsQuery.graphql.swift; sourceTree = ""; }; - EBD833F6580F9B82A68D4A98 /* TokenMarket.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenMarket.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/TokenMarket.graphql.swift; sourceTree = ""; }; - ED157D3DB73302C8A1B9522E /* Query.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Query.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/Query.graphql.swift; sourceTree = ""; }; + D91432EF6C35F5A6301340A2 /* DescriptionTranslations.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DescriptionTranslations.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/DescriptionTranslations.graphql.swift; sourceTree = ""; }; + DB2B02717B1A87BD41920151 /* NftBalance.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftBalance.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftBalance.graphql.swift; sourceTree = ""; }; + DD111A711D83D1D82E4025A9 /* NftApproval.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftApproval.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftApproval.graphql.swift; sourceTree = ""; }; + DD12FD368B3A96F323444D8E /* ProtectionInfo.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ProtectionInfo.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/ProtectionInfo.graphql.swift; sourceTree = ""; }; + DD56CBC543897BE8E37CC2C1 /* TokenApproval.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenApproval.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/TokenApproval.graphql.swift; sourceTree = ""; }; + DE735ED4766E65C0D6808F9A /* NftsQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftsQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/NftsQuery.graphql.swift; sourceTree = ""; }; + E09D12605BAA868AB51573BE /* PortfolioValueModifier.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PortfolioValueModifier.graphql.swift; path = WidgetsCore/MobileSchema/Schema/InputObjects/PortfolioValueModifier.graphql.swift; sourceTree = ""; }; + E0A53C3A09AD259B9A8EDBDD /* PortfolioBalancesQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PortfolioBalancesQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/PortfolioBalancesQuery.graphql.swift; sourceTree = ""; }; + EB9CACD175D6C20B6D4707C8 /* SelectWalletScreenQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SelectWalletScreenQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/SelectWalletScreenQuery.graphql.swift; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; - EE0973BB445AAD8FAA25757F /* NftCollectionScreenQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftCollectionScreenQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/NftCollectionScreenQuery.graphql.swift; sourceTree = ""; }; - F0C34E94906CB9F0A016D566 /* TokenBalanceParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenBalanceParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TokenBalanceParts.graphql.swift; sourceTree = ""; }; - F1A24D364C0D262A14BB7182 /* NftActivity.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftActivity.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftActivity.graphql.swift; sourceTree = ""; }; F1A3F4DDD7E40DA9E4BBAAD1 /* Pods-Uniswap-UniswapTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Uniswap-UniswapTests.release.xcconfig"; path = "Target Support Files/Pods-Uniswap-UniswapTests/Pods-Uniswap-UniswapTests.release.xcconfig"; sourceTree = ""; }; - F2932B486DD7070DF226A27B /* NftAsset.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftAsset.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftAsset.graphql.swift; sourceTree = ""; }; - F3317EBD4E3B11E2F2A1CE58 /* NftTransfer.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftTransfer.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftTransfer.graphql.swift; sourceTree = ""; }; + F286C514E849B29F5235310A /* NftCollectionMarket.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftCollectionMarket.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftCollectionMarket.graphql.swift; sourceTree = ""; }; F35AFD3627EE49230011A725 /* Uniswap.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = Uniswap.entitlements; path = Uniswap/Uniswap.entitlements; sourceTree = ""; }; F35AFD3B27EE49990011A725 /* OneSignalNotificationServiceExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = OneSignalNotificationServiceExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; F35AFD3D27EE49990011A725 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; F35AFD3F27EE49990011A725 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; F35AFD4727EE4B400011A725 /* OneSignalNotificationServiceExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = OneSignalNotificationServiceExtension.entitlements; sourceTree = ""; }; - F4B149117182C6CC58DD570C /* TokenBalance.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenBalance.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/TokenBalance.graphql.swift; sourceTree = ""; }; + F4C3461E088654B80ABE3DE7 /* Amount.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Amount.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/Amount.graphql.swift; sourceTree = ""; }; F56CC08FBB20FAC0DF6B93DA /* Pods-OneSignalNotificationServiceExtension.beta.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OneSignalNotificationServiceExtension.beta.xcconfig"; path = "Target Support Files/Pods-OneSignalNotificationServiceExtension/Pods-OneSignalNotificationServiceExtension.beta.xcconfig"; sourceTree = ""; }; - F5BCBB5D46312050E4A5BD64 /* NftApproveForAll.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftApproveForAll.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftApproveForAll.graphql.swift; sourceTree = ""; }; - F7B3C6DF5DED22E892851B10 /* ContractInput.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ContractInput.graphql.swift; path = WidgetsCore/MobileSchema/Schema/InputObjects/ContractInput.graphql.swift; sourceTree = ""; }; - F912AB9AB67808B740246798 /* NftActivityConnection.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftActivityConnection.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftActivityConnection.graphql.swift; sourceTree = ""; }; - FC419570F5A025E80AB10A72 /* TokenStandard.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenStandard.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/TokenStandard.graphql.swift; sourceTree = ""; }; + F70593F94C8B315F5304B9BE /* NftCollectionEdge.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftCollectionEdge.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftCollectionEdge.graphql.swift; sourceTree = ""; }; + FD158681F2E84A5E49656B11 /* NftOrderEdge.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftOrderEdge.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftOrderEdge.graphql.swift; sourceTree = ""; }; FD54D51C296C79A4007A37E9 /* GoogleServiceInfo */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = GoogleServiceInfo; sourceTree = SOURCE_ROOT; }; FD7304CD28A364FC0085BDEA /* Colors.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Colors.xcassets; path = Uniswap/Colors.xcassets; sourceTree = ""; }; FD7304CF28A3650A0085BDEA /* Colors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Colors.swift; path = Uniswap/Colors.swift; sourceTree = ""; }; + FEE1CB436B299B0349BA9957 /* FeeData.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FeeData.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/FeeData.graphql.swift; sourceTree = ""; }; + FF1B893C0275DAAA156B0108 /* NftApproveForAll.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftApproveForAll.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftApproveForAll.graphql.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -1204,8 +1205,7 @@ 07B067692A7D6EC8001DD9B9 /* Widget */, 03DD298C2A4CE34B00E3E0F5 /* Appearance */, F35AFD3627EE49230011A725 /* Uniswap.entitlements */, - 13B07FAF1A68108700A75B9A /* AppDelegate.h */, - 9FEC9B8A2A858CF1003CD019 /* AppDelegate.m */, + 45FFF7DD2E8C2A3A00362570 /* Notifications */, 6C84F055283D83CF0071FA2E /* Onboarding */, 6CE631B928186D4500716D29 /* WalletConnect */, A32F9FBC272343C8002CFCDB /* GoogleService-Info.plist */, @@ -1213,14 +1213,34 @@ 6CA91BDD2A95226200C4063E /* RNCloudBackupsManager */, 13B07FB51A68108700A75B9A /* Images.xcassets */, 13B07FB61A68108700A75B9A /* Info.plist */, - 13B07FB71A68108700A75B9A /* main.m */, BF9176E944C84910B1C0B057 /* SplashScreen.storyboard */, FD7304CD28A364FC0085BDEA /* Colors.xcassets */, FD7304CF28A3650A0085BDEA /* Colors.swift */, + 8B2A92162EB3E78E00990413 /* AppDelegate.swift */, + 8B2A92182EB3E79500990413 /* Uniswap-Bridging-Header.h */, ); name = Uniswap; sourceTree = ""; }; + 157C3B73F4388F6F1274463F /* Operations */ = { + isa = PBXGroup; + children = ( + 5AF8BB7C2143FF285A912E6A /* Queries */, + ); + name = Operations; + sourceTree = ""; + }; + 281687A9A85A9EF49839548D /* MobileSchema */ = { + isa = PBXGroup; + children = ( + 87442A651C068E552BEA89C6 /* MobileSchema.graphql.swift */, + 308DDBB1BED0BB4E93F1AD8C /* Fragments */, + 157C3B73F4388F6F1274463F /* Operations */, + 4C36556DE2985BABC27C13AD /* Schema */, + ); + name = MobileSchema; + sourceTree = ""; + }; 2D16E6871FA4F8E400B85C8A /* Frameworks */ = { isa = PBXGroup; children = ( @@ -1240,6 +1260,36 @@ name = Frameworks; sourceTree = ""; }; + 308DDBB1BED0BB4E93F1AD8C /* Fragments */ = { + isa = PBXGroup; + children = ( + AB772B265A35BFBC3A7761AD /* HomeScreenTokenParts.graphql.swift */, + AD452B34344D670BA23EA331 /* TokenBalanceMainParts.graphql.swift */, + 65AD13BFAD4EBA18AA6205E3 /* TokenBalanceParts.graphql.swift */, + 5B0A1F5F6FC3E1141E228B7D /* TokenBalanceQuantityParts.graphql.swift */, + D13E592852F52B2855F05A5E /* TokenBasicInfoParts.graphql.swift */, + 2E8286984864AD69A0FAE3BC /* TokenBasicProjectParts.graphql.swift */, + 1166529407CA13E4D4F24F12 /* TokenFeeDataParts.graphql.swift */, + 55081ADF188C967FC1ECD289 /* TokenMarketParts.graphql.swift */, + BB76D1207400E308430A837F /* TokenParts.graphql.swift */, + B5C2E70E43A02405C5CEAD7C /* TokenProjectMarketsParts.graphql.swift */, + 190523DBBA423868C74FF80D /* TokenProjectUrlsParts.graphql.swift */, + 835D9DF76533D22EDF0E9D67 /* TokenProtectionInfoParts.graphql.swift */, + 07F0E504152E22110917353E /* TopTokenParts.graphql.swift */, + ); + name = Fragments; + sourceTree = ""; + }; + 45FFF7DD2E8C2A3A00362570 /* Notifications */ = { + isa = PBXGroup; + children = ( + 45FFF7E02E8C2E6100362570 /* SilentPushEventEmitter.swift */, + 45FFF7DE2E8C2A6400362570 /* SilentPushEventEmitter.m */, + ); + name = Notifications; + path = Uniswap/Notifications; + sourceTree = ""; + }; 47914D9EE3A4DE926EFC5089 /* UniswapTests */ = { isa = PBXGroup; children = ( @@ -1248,27 +1298,18 @@ name = UniswapTests; sourceTree = ""; }; - 4EEA950E66FC2881D331F823 /* Enums */ = { + 4C36556DE2985BABC27C13AD /* Schema */ = { isa = PBXGroup; children = ( - 4A8990A17F489F284E0DB1B3 /* Chain.graphql.swift */, - 25B4C7C7FEF32EAF10D030F9 /* Currency.graphql.swift */, - 3BF42F88002431BFDC9FCA9F /* HistoryDuration.graphql.swift */, - 2D95FE36D2667E070345CDD0 /* NftActivityType.graphql.swift */, - 600250B00B6C02AAB063818B /* NftMarketplace.graphql.swift */, - 4B308EC3DB7C3C1DD5DDE03D /* NftStandard.graphql.swift */, - 40E034582FBD2C374CEF808E /* ProtectionAttackType.graphql.swift */, - BD0FC2D534CB82B151C7F9B7 /* ProtectionResult.graphql.swift */, - 4ACFA5B4204741038C986C5D /* SafetyLevel.graphql.swift */, - D574AB5194929038A54B4D4B /* SwapOrderStatus.graphql.swift */, - 148617FD73F2CD7117960DBA /* SwapOrderType.graphql.swift */, - 32012AC135EBD991C8F02F92 /* TokenSortableField.graphql.swift */, - FC419570F5A025E80AB10A72 /* TokenStandard.graphql.swift */, - CC2FBE9F353D3753FB94B8BF /* TransactionDirection.graphql.swift */, - 2ABA47847A73130D356EB6BF /* TransactionStatus.graphql.swift */, - 69F2FFD11FB5E462CC454A15 /* TransactionType.graphql.swift */, + CF4115ACC8D8F0C7529AABA7 /* SchemaConfiguration.swift */, + 522135E54BDB258B45B5A694 /* SchemaMetadata.graphql.swift */, + F054FF4C0D351C1EAF0012D5 /* Enums */, + F498443CD9DA8803A092F3D9 /* InputObjects */, + C28876A002BC02672EE6A673 /* Interfaces */, + B83DA94147128EE9CC2DB246 /* Objects */, + 971EB6FCC042316AEFF90697 /* Unions */, ); - name = Enums; + name = Schema; sourceTree = ""; }; 5754C6A1B51170788A63F6F3 /* ExpoModulesProviders */ = { @@ -1283,11 +1324,39 @@ 5841D897B122046172ACD989 /* WidgetsCore */ = { isa = PBXGroup; children = ( - 8D57A3CBC005A567F7303961 /* MobileSchema */, + 281687A9A85A9EF49839548D /* MobileSchema */, ); name = WidgetsCore; sourceTree = ""; }; + 5AF8BB7C2143FF285A912E6A /* Queries */ = { + isa = PBXGroup; + children = ( + 6E4DEE7FC8D7BCD030CFA4D3 /* ConvertQuery.graphql.swift */, + 3B03A0A6AB0957A512A2D225 /* FavoriteTokenCardQuery.graphql.swift */, + 9EFADDBB38BB9261933C1349 /* FeedTransactionListQuery.graphql.swift */, + 0E7FF51A507BF788E64E3EFE /* HomeScreenTokensQuery.graphql.swift */, + 595699514939A2CE780AF0CF /* MultiplePortfolioBalancesQuery.graphql.swift */, + 03AB6B21A1BFB3A18982B47E /* NFTItemScreenQuery.graphql.swift */, + CA3BFDC64C5E0A058877A348 /* NftCollectionScreenQuery.graphql.swift */, + DE735ED4766E65C0D6808F9A /* NftsQuery.graphql.swift */, + BA3BCD49511DE84892B34ED0 /* NftsTabQuery.graphql.swift */, + E0A53C3A09AD259B9A8EDBDD /* PortfolioBalancesQuery.graphql.swift */, + EB9CACD175D6C20B6D4707C8 /* SelectWalletScreenQuery.graphql.swift */, + 957A35D2DE7A140BA198A0F1 /* TokenDetailsScreenQuery.graphql.swift */, + 9602E2A290179F8741EDD35A /* TokenPriceHistoryQuery.graphql.swift */, + 1276D33B06E840B8E6839F39 /* TokenProjectDescriptionQuery.graphql.swift */, + A98CEFE7189D63880FD5A702 /* TokenProjectsQuery.graphql.swift */, + 0914ECF70C274E2B064BCD42 /* TokenQuery.graphql.swift */, + D4C8174A3A9EB3244AA844F4 /* TokensQuery.graphql.swift */, + 243576FAC8D2801F9D99ECC9 /* TopTokensQuery.graphql.swift */, + 559CBC7F568C769C48E7C6F4 /* TransactionHistoryUpdaterQuery.graphql.swift */, + 95283AC560844B50E95934E4 /* TransactionListQuery.graphql.swift */, + 6F0A91F56B1CA2E71B479BAD /* WidgetTokensQuery.graphql.swift */, + ); + name = Queries; + sourceTree = ""; + }; 5B4398EB2DD3B22C00F6BE08 /* PrivateKeyDisplay */ = { isa = PBXGroup; children = ( @@ -1403,17 +1472,6 @@ name = Products; sourceTree = ""; }; - 8D57A3CBC005A567F7303961 /* MobileSchema */ = { - isa = PBXGroup; - children = ( - A4E0C448C5D45BF1071FA458 /* MobileSchema.graphql.swift */, - CA8FF4CDF9B448F7BE77CA07 /* Fragments */, - F79593090BE522E6D2A3330F /* Operations */, - 9723B33A3716C9068F5FD2B1 /* Schema */, - ); - name = MobileSchema; - sourceTree = ""; - }; 8E566D9F2AA1095000D4AA76 /* Components */ = { isa = PBXGroup; children = ( @@ -1462,18 +1520,13 @@ path = Uniswap/Icons; sourceTree = ""; }; - 9723B33A3716C9068F5FD2B1 /* Schema */ = { + 971EB6FCC042316AEFF90697 /* Unions */ = { isa = PBXGroup; children = ( - 2E49E724432C70A2551FB2C7 /* SchemaConfiguration.swift */, - C65195A1CC9894C6341385C3 /* SchemaMetadata.graphql.swift */, - 4EEA950E66FC2881D331F823 /* Enums */, - B741FF3580D33B5968CB9841 /* InputObjects */, - A60630E00A276B08BE85275D /* Interfaces */, - AB0BCF231B03A3F4E547DFF5 /* Objects */, - F23767E14A7943D5969AFDC1 /* Unions */, + 075E4C1DFAA00EE5B3CE792D /* ActivityDetails.graphql.swift */, + 6D4C849A7805FCCEE8C8C3E6 /* AssetChange.graphql.swift */, ); - name = Schema; + name = Unions; sourceTree = ""; }; 9759A762F61D6B2F01C79DBF /* Uniswap */ = { @@ -1484,87 +1537,72 @@ name = Uniswap; sourceTree = ""; }; - A60630E00A276B08BE85275D /* Interfaces */ = { + B83DA94147128EE9CC2DB246 /* Objects */ = { isa = PBXGroup; children = ( - 03AD8C2B1226EBE1524F2573 /* IAmount.graphql.swift */, - 5E0D68CC8D1F13D864F069BB /* IContract.graphql.swift */, - ); - name = Interfaces; - sourceTree = ""; - }; - AB0BCF231B03A3F4E547DFF5 /* Objects */ = { - isa = PBXGroup; - children = ( - D20E9461A6C9C1CD6A24BEED /* Amount.graphql.swift */, - E63EA0C75D8011BCC182492C /* AmountChange.graphql.swift */, - 608B07304ED1387B5AEAA3BB /* ApplicationContract.graphql.swift */, - E4624A41EC1468712BD77653 /* AssetActivity.graphql.swift */, - BD35300F81746B2D4A23065A /* BlockaidFees.graphql.swift */, - 7E1F288B7EDDE906C4C00C46 /* DescriptionTranslations.graphql.swift */, - 54B1C71AED738D17898103A6 /* Dimensions.graphql.swift */, - 17819A49DE69723EC60D9D72 /* FeeData.graphql.swift */, - 31ED613D63FE9C56D9270517 /* Image.graphql.swift */, - 8BEADEB4AE05B860CF2232D1 /* NetworkFee.graphql.swift */, - F1A24D364C0D262A14BB7182 /* NftActivity.graphql.swift */, - F912AB9AB67808B740246798 /* NftActivityConnection.graphql.swift */, - 4DB4379B5E222207CA7328D0 /* NftActivityEdge.graphql.swift */, - 4E32BBB65A6DD9CFF608C8E8 /* NftApproval.graphql.swift */, - F5BCBB5D46312050E4A5BD64 /* NftApproveForAll.graphql.swift */, - F2932B486DD7070DF226A27B /* NftAsset.graphql.swift */, - 218B8AB05C80D919671D3970 /* NftAssetConnection.graphql.swift */, - 6DF7C8888BE6AFA2F22889FE /* NftAssetEdge.graphql.swift */, - 70851E3D1A4B296A1CE22A20 /* NftAssetTrait.graphql.swift */, - 05BE2F49DD1FB9A692E21C13 /* NftBalance.graphql.swift */, - 1FC7B80EC0E0D2134B67575B /* NftBalanceConnection.graphql.swift */, - 0B132543F80E3C9191219E6D /* NftBalanceEdge.graphql.swift */, - 63ACFF2A1D5AE3498731AC69 /* NftCollection.graphql.swift */, - 92DE390311705FF1EE65C0E6 /* NftCollectionConnection.graphql.swift */, - 37038CCEB11E70E2027EF4EA /* NftCollectionEdge.graphql.swift */, - 4F4F47D22B749A438B5BF674 /* NftCollectionMarket.graphql.swift */, - 18B9EB43501127BE1B14F1A5 /* NftContract.graphql.swift */, - 6E3A78386B3B779B688021FF /* NftOrder.graphql.swift */, - 39FC2A13550FA6754EC1A7FC /* NftOrderConnection.graphql.swift */, - 49BD6C776029FAF4FC711DE7 /* NftOrderEdge.graphql.swift */, - DB35E518AD7FBFEBEA9CAB95 /* NftProfile.graphql.swift */, - F3317EBD4E3B11E2F2A1CE58 /* NftTransfer.graphql.swift */, - 396B6AB3BB41898B56EB3828 /* OffRampTransactionDetails.graphql.swift */, - C9D5E6A07F404974045BD525 /* OffRampTransfer.graphql.swift */, - 6A84A192F750A0F1BC8D7A69 /* OnRampServiceProvider.graphql.swift */, - 12D3AB6C2FC9513E9F224B7C /* OnRampTransactionDetails.graphql.swift */, - 2139ABF56EA067ED792A48C9 /* OnRampTransfer.graphql.swift */, - E475EBABF7EA310435E2F19C /* PageInfo.graphql.swift */, - 1849039FD4A210DA990363C8 /* Portfolio.graphql.swift */, - 0702203B17BDD858CED28F8B /* ProtectionInfo.graphql.swift */, - ED157D3DB73302C8A1B9522E /* Query.graphql.swift */, - C955CE2592785712A11892DE /* SwapOrderDetails.graphql.swift */, - B0DB67D6C438F623976727E9 /* TimestampedAmount.graphql.swift */, - 647933A5DC35BDFEEF3620A1 /* Token.graphql.swift */, - 9E975BEDBED5FE51D0DC6E96 /* TokenApproval.graphql.swift */, - F4B149117182C6CC58DD570C /* TokenBalance.graphql.swift */, - EBD833F6580F9B82A68D4A98 /* TokenMarket.graphql.swift */, - 4981A14906A35E8A0B7F9457 /* TokenProject.graphql.swift */, - 3B9A505509D9A85BE1DB7D05 /* TokenProjectMarket.graphql.swift */, - 8DA4E7C052A6C4AC3A9DCDA6 /* TokenTransfer.graphql.swift */, - ABE51731EB853137302A7B0D /* TransactionDetails.graphql.swift */, + F4C3461E088654B80ABE3DE7 /* Amount.graphql.swift */, + 066BBBD815CF2DE8803338BC /* AmountChange.graphql.swift */, + 59DD4D834D7B42A6D02F4F12 /* ApplicationContract.graphql.swift */, + 05C66C49AF2B78703FCE4C7C /* AssetActivity.graphql.swift */, + 3EF9AEC00868BFD7CEBF202C /* BlockaidFees.graphql.swift */, + CAC62CDEEBE5712219A457C7 /* BridgedWithdrawalInfo.graphql.swift */, + D91432EF6C35F5A6301340A2 /* DescriptionTranslations.graphql.swift */, + 764C4757EB194D13A8857DFA /* Dimensions.graphql.swift */, + FEE1CB436B299B0349BA9957 /* FeeData.graphql.swift */, + 8C9047B981FA322A727947F7 /* Image.graphql.swift */, + 09BE4B220A4F8465C3D01BA5 /* NetworkFee.graphql.swift */, + 55C67E53A2C8AEB6AE92E33F /* NftActivity.graphql.swift */, + CBE0699A65BEEEE6E9A6B03F /* NftActivityConnection.graphql.swift */, + 043C7D80D6722AFCF1715D07 /* NftActivityEdge.graphql.swift */, + DD111A711D83D1D82E4025A9 /* NftApproval.graphql.swift */, + FF1B893C0275DAAA156B0108 /* NftApproveForAll.graphql.swift */, + 5C9F3B31FC6AF8CE30CD7DA2 /* NftAsset.graphql.swift */, + AD86EFAA0D6B8FBECCC4828B /* NftAssetConnection.graphql.swift */, + BFFD4C1E5A4B30DE3CE02DB4 /* NftAssetEdge.graphql.swift */, + 85B43D489CC5ACF1EA34E9EB /* NftAssetTrait.graphql.swift */, + DB2B02717B1A87BD41920151 /* NftBalance.graphql.swift */, + 62DDC1F42E7172783EAF3776 /* NftBalanceConnection.graphql.swift */, + 4DD55DFFC717D37DD960B82E /* NftBalanceEdge.graphql.swift */, + B25620D6E0E297C56198F190 /* NftCollection.graphql.swift */, + 0D09FFE2B5E56CDCAB2F9455 /* NftCollectionConnection.graphql.swift */, + F70593F94C8B315F5304B9BE /* NftCollectionEdge.graphql.swift */, + F286C514E849B29F5235310A /* NftCollectionMarket.graphql.swift */, + 55A243177E9FA92A0524A222 /* NftContract.graphql.swift */, + 67C0D95F7EA9E7ACA9B18692 /* NftOrder.graphql.swift */, + 98F489175099FE697721272D /* NftOrderConnection.graphql.swift */, + FD158681F2E84A5E49656B11 /* NftOrderEdge.graphql.swift */, + AE2A9FBFB7F718246B668A60 /* NftProfile.graphql.swift */, + 74AD16E33DC518A42C386FE2 /* NftTransfer.graphql.swift */, + C4CA32B2B2997D73A0185635 /* OffRampTransactionDetails.graphql.swift */, + 166A9AE7203545C397E683E8 /* OffRampTransfer.graphql.swift */, + 5BED80DB034487FCA1450EDB /* OnRampServiceProvider.graphql.swift */, + 87CF9B03AE29A19C5B0F4E35 /* OnRampTransactionDetails.graphql.swift */, + 3E5D1A04B42B25218C53C107 /* OnRampTransfer.graphql.swift */, + 97FBBE677B56FFE07052EF1D /* PageInfo.graphql.swift */, + 5397E77A577E9952D2477020 /* Portfolio.graphql.swift */, + DD12FD368B3A96F323444D8E /* ProtectionInfo.graphql.swift */, + 5956ECBE73216D266D8D2E86 /* Query.graphql.swift */, + 53F4E5E9341BE66984935185 /* SwapOrderDetails.graphql.swift */, + 31CBA4397C44A04E218AEF28 /* TimestampedAmount.graphql.swift */, + 2FB1590D18A58D37C750D9AC /* Token.graphql.swift */, + DD56CBC543897BE8E37CC2C1 /* TokenApproval.graphql.swift */, + 181209B4CDE80BDC80891CFD /* TokenBalance.graphql.swift */, + C8E5B09464C768590127BFA6 /* TokenMarket.graphql.swift */, + 8856DFFC7FDC9BF105B0B4E3 /* TokenProject.graphql.swift */, + 0B0505DB30407C221B6A8491 /* TokenProjectMarket.graphql.swift */, + 9BEBBC668E69EE3A1FC6AB05 /* TokenTransfer.graphql.swift */, + 55240C84512D3851C8B9F027 /* TransactionDetails.graphql.swift */, ); name = Objects; sourceTree = ""; }; - B741FF3580D33B5968CB9841 /* InputObjects */ = { + C28876A002BC02672EE6A673 /* Interfaces */ = { isa = PBXGroup; children = ( - F7B3C6DF5DED22E892851B10 /* ContractInput.graphql.swift */, - 43E2ECCEFBC3BE8D13BE6CAB /* NftActivityFilterInput.graphql.swift */, - 556339705BD103365D4ED07B /* NftAssetTraitInput.graphql.swift */, - 8B477EBEA1EDA3914C77E8A6 /* NftAssetsFilterInput.graphql.swift */, - 4E5FA43C5E31035775917C4E /* NftBalanceAssetInput.graphql.swift */, - 41BA2129FFE961C8549C8A30 /* NftBalancesFilterInput.graphql.swift */, - C4790C2A43570CAC086B936E /* NftCollectionsFilterInput.graphql.swift */, - 315B8A905FA2C60C053F138B /* OnRampTransactionsAuth.graphql.swift */, - 41003491EF939043CF9D0F4E /* PortfolioValueModifier.graphql.swift */, + 37376E4CF3A4B76E9DAE150C /* IAmount.graphql.swift */, + 1D9189C818AFF8E988F861D0 /* IContract.graphql.swift */, ); - name = InputObjects; + name = Interfaces; sourceTree = ""; }; C2C18ECBEF5A4489BF3A314C /* Resources */ = { @@ -1577,56 +1615,6 @@ name = Resources; sourceTree = ""; }; - CA8FF4CDF9B448F7BE77CA07 /* Fragments */ = { - isa = PBXGroup; - children = ( - 27F8B915042BD8D67D9627EA /* HomeScreenTokenParts.graphql.swift */, - 137696908D703632CEE3AB28 /* TokenBalanceMainParts.graphql.swift */, - F0C34E94906CB9F0A016D566 /* TokenBalanceParts.graphql.swift */, - 2370BAD675DC8064C00AC037 /* TokenBalanceQuantityParts.graphql.swift */, - 3377F7BB38BD5A45AE31909D /* TokenBasicInfoParts.graphql.swift */, - 2C2F4BE3080D4129D4478528 /* TokenBasicProjectParts.graphql.swift */, - 018603D0FA4D05DBF16F1441 /* TokenFeeDataParts.graphql.swift */, - BFC36D963A2A52D1E7CA77E4 /* TokenMarketParts.graphql.swift */, - 98F596250F55A3D6E9E1F499 /* TokenParts.graphql.swift */, - CC055C3EDFDE2966FCA83C9E /* TokenProjectMarketsParts.graphql.swift */, - 236126979349098B98C70F27 /* TokenProjectUrlsParts.graphql.swift */, - 0BB4A40E30E1D985519DB3CF /* TokenProtectionInfoParts.graphql.swift */, - 5640432978873FB030467750 /* TopTokenParts.graphql.swift */, - ); - name = Fragments; - sourceTree = ""; - }; - D9DDAE0E99B4703AF96A81AB /* Queries */ = { - isa = PBXGroup; - children = ( - 7BD3F0794908635CDFA7DAB6 /* ConvertQuery.graphql.swift */, - 84997376A0B436ECC0A996C5 /* ExploreSearchQuery.graphql.swift */, - 3F0EEAC1DADE827AE38EB8A6 /* FavoriteTokenCardQuery.graphql.swift */, - 2BA0ACE5B9B2C41187CD41F3 /* FeedTransactionListQuery.graphql.swift */, - B7E03AA02B39A7A90C96AA76 /* HomeScreenTokensQuery.graphql.swift */, - 4429CA6DA254ECC24A65DA14 /* MultiplePortfolioBalancesQuery.graphql.swift */, - 207F4DF7664454C31DE069F8 /* NFTItemScreenQuery.graphql.swift */, - EE0973BB445AAD8FAA25757F /* NftCollectionScreenQuery.graphql.swift */, - A9030AC59702756C39396FFE /* NftsQuery.graphql.swift */, - 730CA356D6E1D498CC0C1472 /* NftsTabQuery.graphql.swift */, - 985DDC8324B872DD44E87781 /* PortfolioBalancesQuery.graphql.swift */, - 8A5342DEF1410E0E81E97F40 /* SearchTokensQuery.graphql.swift */, - B033206DC974BCB6AF8CC613 /* SelectWalletScreenQuery.graphql.swift */, - A7A33933670CCAD9E62A1A80 /* TokenDetailsScreenQuery.graphql.swift */, - 9DFBD29C0BDFDB4464B8189C /* TokenPriceHistoryQuery.graphql.swift */, - 6FF8B660E9C2C0D8DDF7588A /* TokenProjectDescriptionQuery.graphql.swift */, - E8A49D011C60137D4034CAC4 /* TokenProjectsQuery.graphql.swift */, - 74885865ED8CFEA875E40916 /* TokenQuery.graphql.swift */, - B3C3F0403FAEFDD632AC6CCE /* TokensQuery.graphql.swift */, - 04E5C2900254E7BA7F10CE51 /* TopTokensQuery.graphql.swift */, - AF3D7A923E5E1FB504E1F64D /* TransactionHistoryUpdaterQuery.graphql.swift */, - 224CA82F1871016F4173F69E /* TransactionListQuery.graphql.swift */, - DBA33683D2BA6BBE3251D67C /* WidgetTokensQuery.graphql.swift */, - ); - name = Queries; - sourceTree = ""; - }; E233CBF5F47BEE60B243DCF8 /* Pods */ = { isa = PBXGroup; children = ( @@ -1662,13 +1650,27 @@ path = Pods; sourceTree = ""; }; - F23767E14A7943D5969AFDC1 /* Unions */ = { + F054FF4C0D351C1EAF0012D5 /* Enums */ = { isa = PBXGroup; children = ( - 8A4EB7BAD2C90E61FD6C30AF /* ActivityDetails.graphql.swift */, - 39F60B2B54796DC978765C99 /* AssetChange.graphql.swift */, + 7DBCFBC330BF97BA2F630908 /* Chain.graphql.swift */, + CD0CA37FB709C06F123EBF74 /* Currency.graphql.swift */, + 1053483279043EED7612BE90 /* HistoryDuration.graphql.swift */, + 1CDAC035887C3908C3BD1A77 /* NftActivityType.graphql.swift */, + 83FB27D93A9468E6DB47231C /* NftMarketplace.graphql.swift */, + 0373C61A7AF015058A502CE2 /* NftStandard.graphql.swift */, + B3F4D5E8B9ED6E0FE29EA779 /* ProtectionAttackType.graphql.swift */, + AAB6B8C64F7525D0D0E978C6 /* ProtectionResult.graphql.swift */, + 22F8093091A9E5FC0D6284BA /* SafetyLevel.graphql.swift */, + C0359DDEA2E2E17759FCC5CF /* SwapOrderStatus.graphql.swift */, + 7AFDF569E43F173C2F5A07AF /* SwapOrderType.graphql.swift */, + 51F133663B435829DE47E3CE /* TokenSortableField.graphql.swift */, + D34AC1057AD14362BB91E88B /* TokenStandard.graphql.swift */, + 64D494D944670E557175A694 /* TransactionDirection.graphql.swift */, + 945EF0A3BD6F19DA14E40AE1 /* TransactionStatus.graphql.swift */, + 18D48B25E03122149B0D1304 /* TransactionType.graphql.swift */, ); - name = Unions; + name = Enums; sourceTree = ""; }; F35AFD3C27EE49990011A725 /* OneSignalNotificationServiceExtension */ = { @@ -1682,12 +1684,19 @@ path = OneSignalNotificationServiceExtension; sourceTree = ""; }; - F79593090BE522E6D2A3330F /* Operations */ = { + F498443CD9DA8803A092F3D9 /* InputObjects */ = { isa = PBXGroup; children = ( - D9DDAE0E99B4703AF96A81AB /* Queries */, + AED050A208757F137DBDA57D /* ContractInput.graphql.swift */, + 50C3E075E850CEC1714BE52C /* NftActivityFilterInput.graphql.swift */, + 1F1731869775D16A9EAB475C /* NftAssetTraitInput.graphql.swift */, + 310373063D8A11E9E02314FE /* NftAssetsFilterInput.graphql.swift */, + 696F6C5220D0072E380E198F /* NftBalanceAssetInput.graphql.swift */, + 1810C60B56CC74150C868801 /* NftBalancesFilterInput.graphql.swift */, + B3CC3D9DE64A39897BA580B3 /* OnRampTransactionsAuth.graphql.swift */, + E09D12605BAA868AB51573BE /* PortfolioValueModifier.graphql.swift */, ); - name = Operations; + name = InputObjects; sourceTree = ""; }; /* End PBXGroup section */ @@ -1822,7 +1831,7 @@ 9F7898182A819D62004D5A98 /* Embed Frameworks */, 163678CCBB906C7B12421609 /* [CP] Embed Pods Frameworks */, 0487071ABBC71F28EF79F4AA /* [CP] Copy Pods Resources */, - A98B71284330BF95E7CAB037 /* [CP-User] [RNFB] Core Configuration */, + 868B6279F959D6931E7A870E /* [CP-User] [RNFB] Core Configuration */, ); buildRules = ( ); @@ -1991,7 +2000,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "export PROJECT_ROOT=$PWD/..\nexport EXTRA_PACKAGER_ARGS=\"--sourcemap-output $PROJECT_ROOT/main.jsbundle.map\"\n\nset -e\nif [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\nsource \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\nsource \"$PODS_ROOT/../.xcode.env.local\"\nfi\nexport CONFIG_CMD=\"dummy-workaround-value\"\nexport CLI_PATH=\"$(\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('@rnef/cli/package.json')) + '/dist/src/bin.js'\")\"\n\nWITH_ENVIRONMENT=\"../../../node_modules/react-native/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"../../../node_modules/react-native/scripts/react-native-xcode.sh\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT $REACT_NATIVE_XCODE\"\n"; + shellScript = "if [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\n source \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n# The project root by default is one level up from the ios directory\nexport PROJECT_ROOT=\"$PROJECT_DIR\"/..\n\nif [[ \"$CONFIGURATION\" = *Debug* ]]; then\n export SKIP_BUNDLING=1\nfi\nif [[ -z \"$ENTRY_FILE\" ]]; then\n # Set the entry JS file using the bundler's entry resolution.\n export ENTRY_FILE=\"$(\"$NODE_BINARY\" -e \"require('expo/scripts/resolveAppEntry')\" \"$PROJECT_ROOT\" ios absolute | tail -n 1)\"\nfi\n\nif [[ -z \"$CLI_PATH\" ]]; then\n # Use Expo CLI\n export CLI_PATH=\"$(\"$NODE_BINARY\" --print \"require.resolve('@expo/cli')\")\"\nfi\nif [[ -z \"$BUNDLE_COMMAND\" ]]; then\n # Default Expo CLI command for bundling\n export BUNDLE_COMMAND=\"export:embed\"\nfi\n\n`\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\"`\n"; }; 0487071ABBC71F28EF79F4AA /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; @@ -2149,6 +2158,7 @@ "$(SRCROOT)/WidgetsCore/MobileSchema/Schema/Objects/ApplicationContract.graphql.swift", "$(SRCROOT)/WidgetsCore/MobileSchema/Schema/Objects/AssetActivity.graphql.swift", "$(SRCROOT)/WidgetsCore/MobileSchema/Schema/Objects/BlockaidFees.graphql.swift", + "$(SRCROOT)/WidgetsCore/MobileSchema/Schema/Objects/BridgedWithdrawalInfo.graphql.swift", "$(SRCROOT)/WidgetsCore/MobileSchema/Schema/Objects/DescriptionTranslations.graphql.swift", "$(SRCROOT)/WidgetsCore/MobileSchema/Schema/Objects/Dimensions.graphql.swift", "$(SRCROOT)/WidgetsCore/MobileSchema/Schema/Objects/FeeData.graphql.swift", @@ -2387,7 +2397,7 @@ 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; }; - A98B71284330BF95E7CAB037 /* [CP-User] [RNFB] Core Configuration */ = { + 868B6279F959D6931E7A870E /* [CP-User] [RNFB] Core Configuration */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -2416,7 +2426,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "set -e\nexport SOURCEMAP_FILE=$DERIVED_FILE_DIR/main.jsbundle.map\n\nset -e\nif [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\nsource \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\nsource \"$PODS_ROOT/../.xcode.env.local\"\nfi\nexport CONFIG_CMD=\"dummy-workaround-value\"\nexport CLI_PATH=\"$(\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('@rnef/cli/package.json')) + '/dist/src/bin.js'\")\"\n\nWITH_ENVIRONMENT=\"../../../node_modules/react-native/scripts/xcode/with-environment.sh\"\nDATADOG_XCODE=\"./sourcemaps-datadog.sh\"\n\nif [[ -n \"$DATADOG_API_KEY\" ]]; then\n # JS source maps\n /bin/sh -c \"$WITH_ENVIRONMENT $DATADOG_XCODE\"\n # iOS dSYM\n ../../../node_modules/.bin/datadog-ci dsyms upload $DWARF_DSYM_FOLDER_PATH\nelse\n echo \"Ignoring upload step for local, API key is missing.\"\nfi\n"; + shellScript = "set -e\n\nWITH_ENVIRONMENT=\"../../../node_modules/react-native/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"./sourcemaps-datadog.sh\"\n\nexport SOURCEMAP_FILE=$DERIVED_FILE_DIR/main.jsbundle.map\n\nif [[ -n \"$DATADOG_API_KEY\" && \"$SKIP_DATADOG_UPLOAD\" != \"true\" ]]; then\n echo \"warning: Starting Datadog Uploads\"\n echo \"warning: Build Configuration: $CONFIGURATION\"\n echo \"warning: Product Name: $PRODUCT_NAME\"\n echo \"warning: Source Map File: $SOURCEMAP_FILE\"\n echo \"warning: dSYM Path: $DWARF_DSYM_FOLDER_PATH\"\n echo \"\"\n\n # JS source maps\n echo \"warning: Uploading JS source maps...\"\n \n /bin/sh -c \"$WITH_ENVIRONMENT $REACT_NATIVE_XCODE\"\n\n echo \"warning: \"\n\n # iOS dSYM\n echo \"warning: Uploading iOS dSYMs...\"\n echo \"warning: dSYM command: datadog-ci dsyms upload $DWARF_DSYM_FOLDER_PATH\"\n\n DSYM_LOG=$(mktemp)\n if ../../../node_modules/.bin/datadog-ci dsyms upload $DWARF_DSYM_FOLDER_PATH 2>&1 | tee \"$DSYM_LOG\"; then\n echo \"warning: dSYM upload completed successfully\"\n rm -f \"$DSYM_LOG\"\n else\n DSYM_EXIT_CODE=$?\n echo \"warning: \"\n echo \"warning: dSYM Upload Failed\"\n echo \"warning: Exit Code: $DSYM_EXIT_CODE\"\n echo \"warning: \"\n echo \"warning:Full Error Output:\"\n echo \"warning: ---\"\n echo \"warning: $(cat \"$DSYM_LOG\")\"\n echo \"warning: ---\"\n echo \"warning: \"\n echo \"warning: Debug Information:\"\n echo \"warning: - datadog-ci version: $(../../../node_modules/.bin/datadog-ci version 2>&1 || echo 'Failed to get version')\"\n echo \"warning: - dSYM folder exists: $([ -d \\\"$DWARF_DSYM_FOLDER_PATH\\\" ] && echo 'Yes' || echo 'No')\"\n echo \"warning: - dSYM contents: $(ls -la \\\"$DWARF_DSYM_FOLDER_PATH\\\" 2>&1 || echo 'Failed to list')\"\n echo \"warning: - DATADOG_API_KEY set: $([ -n \\\"$DATADOG_API_KEY\\\" ] && echo 'Yes (${#DATADOG_API_KEY} chars)' || echo 'No')\"\n echo \"warning: - Working directory: $(pwd)\"\n echo \"warning: \"\n echo \"warning: This is non-critical. Build will continue.\"\n rm -f \"$DSYM_LOG\"\n fi\n\n echo \"warning: \"\n echo \"warning: Datadog upload phase completed (build continues regardless of upload status)\"\nelse\n echo \"warning: Skipping Datadog upload (DATADOG_API_KEY not set or SKIP_DATADOG_UPLOAD=true)\"\nfi\n\n# Always exit 0 to not fail the build\nexit 0\n"; }; F5C7F44CBF58F052A43EB4AA /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; @@ -2473,7 +2483,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "export RCT_METRO_PORT=\"${RCT_METRO_PORT:=8081}\"\necho \"export RCT_METRO_PORT=${RCT_METRO_PORT}\" > \"${SRCROOT}/../node_modules/react-native/scripts/.packager.env\"\nif [ -z \"${RCT_NO_LAUNCH_PACKAGER+xxx}\" ] ; then\n if nc -w 5 -z localhost ${RCT_METRO_PORT} ; then\n if ! curl -s \"http://localhost:${RCT_METRO_PORT}/status\" | grep -q \"packager-status:running\" ; then\n echo \"Port ${RCT_METRO_PORT} already in use, packager is either not running or not running correctly\"\n exit 2\n fi\n else\n open \"$SRCROOT/../node_modules/react-native/scripts/launchPackager.command\" || echo \"Can't start packager automatically\"\n fi\nfi\n"; + shellScript = "if [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\n source \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\nexport RCT_METRO_PORT=\"${RCT_METRO_PORT:=8081}\"\necho \"export RCT_METRO_PORT=${RCT_METRO_PORT}\" > `$NODE_BINARY --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/.packager.env'\"`\nif [ -z \"${RCT_NO_LAUNCH_PACKAGER+xxx}\" ] ; then\n if nc -w 5 -z localhost ${RCT_METRO_PORT} ; then\n if ! curl -s \"http://localhost:${RCT_METRO_PORT}/status\" | grep -q \"packager-status:running\" ; then\n echo \"Port ${RCT_METRO_PORT} already in use, packager is either not running or not running correctly\"\n exit 2\n fi\n else\n open `$NODE_BINARY --print \"require('path').dirname(require.resolve('expo/package.json')) + '/scripts/launchPackager.command'\"` || echo \"Can't start packager automatically\"\n fi\nfi\n"; showEnvVarsInLog = 0; }; FD54D51B296C780A007A37E9 /* Copy configuration-specific GoogleServices-Info.plist */ = { @@ -2640,122 +2650,123 @@ 0743221A2A83E3CA00F8518D /* TokenMarket.graphql.swift in Sources */, 074322322A83E3CA00F8518D /* NftBalance.graphql.swift in Sources */, 9F813EA02AA8FB7500438D89 /* TransactionDetails.graphql.swift in Sources */, - 30872BFB39EEC66944E3EFD7 /* MobileSchema.graphql.swift in Sources */, - 9822D243ED2F5C350404A375 /* HomeScreenTokenParts.graphql.swift in Sources */, - 61988A922656857278F7CBF9 /* TokenBalanceMainParts.graphql.swift in Sources */, - C8402101FF510BAA358DCA46 /* TokenBalanceParts.graphql.swift in Sources */, - FA8EB6A63AB3F56194C6CFB8 /* TokenBalanceQuantityParts.graphql.swift in Sources */, - A974633048E27D5D23420F34 /* TokenBasicInfoParts.graphql.swift in Sources */, - C8338C5BE951EF9111C48217 /* TokenBasicProjectParts.graphql.swift in Sources */, - 09F9DEB33392F3051BEA8D52 /* TokenFeeDataParts.graphql.swift in Sources */, - D7E3642851C618D369D26B82 /* TokenMarketParts.graphql.swift in Sources */, - 302E24504C4FCE674CF95984 /* TokenParts.graphql.swift in Sources */, - F53121FB1B070DB9A81347DF /* TokenProjectMarketsParts.graphql.swift in Sources */, - E0F63BA3A99DB10FA1CCAB5E /* TokenProjectUrlsParts.graphql.swift in Sources */, - 2B12DFE797E2CBF314565D81 /* TokenProtectionInfoParts.graphql.swift in Sources */, - 71CF37F19F1C138B57CC135C /* TopTokenParts.graphql.swift in Sources */, - 93566FBDE94E1A2D8CC5AB62 /* ConvertQuery.graphql.swift in Sources */, - 63BDDE4499AC6B2375C9F795 /* FavoriteTokenCardQuery.graphql.swift in Sources */, - FD4E55146E046C7F5983A379 /* FeedTransactionListQuery.graphql.swift in Sources */, - F0D6A92BB4FCE19CDFA5BBE1 /* HomeScreenTokensQuery.graphql.swift in Sources */, - D660CA59775C3634324037ED /* MultiplePortfolioBalancesQuery.graphql.swift in Sources */, - 2B422D705C68BB51FBDA9895 /* NFTItemScreenQuery.graphql.swift in Sources */, - EF3D92CA76DEE66090F18D0C /* NftCollectionScreenQuery.graphql.swift in Sources */, - AC70FF8207ED26561B634E30 /* NftsQuery.graphql.swift in Sources */, - 1C3559D557BC09C709104802 /* NftsTabQuery.graphql.swift in Sources */, - A104024A1861354EC1DD53C1 /* PortfolioBalancesQuery.graphql.swift in Sources */, - F772B09295DABC5E8895C1C5 /* SelectWalletScreenQuery.graphql.swift in Sources */, - 9381B5EA5839DA17D07A45F0 /* TokenDetailsScreenQuery.graphql.swift in Sources */, - DC0641C5870F91D26C914F1D /* TokenPriceHistoryQuery.graphql.swift in Sources */, - 93AEECDBDB160B5E0C9E3149 /* TokenProjectDescriptionQuery.graphql.swift in Sources */, - 8CEA6459A5B739C3A0382000 /* TokenProjectsQuery.graphql.swift in Sources */, - 677FC15A05D9BD23930FAC95 /* TokenQuery.graphql.swift in Sources */, - 62B32E9623DA0E939F062033 /* TokensQuery.graphql.swift in Sources */, - 171DD1C966C7FBF9DD68CFAC /* TopTokensQuery.graphql.swift in Sources */, - 257C7A9F2C4AC3C99C6B7348 /* TransactionHistoryUpdaterQuery.graphql.swift in Sources */, - 3361FE5837059AEF4AA877BE /* TransactionListQuery.graphql.swift in Sources */, - 8410B98A8D7974A941AAF299 /* WidgetTokensQuery.graphql.swift in Sources */, - 9AF0D1FDF2BFB17FB732C5FD /* SchemaConfiguration.swift in Sources */, - 8950F8354810AA673E1E35DA /* SchemaMetadata.graphql.swift in Sources */, - 4917B04DB81579CF4243A1DA /* Chain.graphql.swift in Sources */, - 614FCC3D8E9AA8175E950726 /* Currency.graphql.swift in Sources */, - 326CFFDBB89D95FA4CB95EBB /* HistoryDuration.graphql.swift in Sources */, - 143C638C2CCAC689371BFC93 /* NftActivityType.graphql.swift in Sources */, - 648B919F00D069DE1F0040F6 /* NftMarketplace.graphql.swift in Sources */, - 7BE97D20155AE69FF36C4CB4 /* NftStandard.graphql.swift in Sources */, - 1BC3A49161EB906542F8E23B /* ProtectionAttackType.graphql.swift in Sources */, - 8FE0F30936E893297558F467 /* ProtectionResult.graphql.swift in Sources */, - 66D2765A00D8CE3136D28F1F /* SafetyLevel.graphql.swift in Sources */, - 85ADD03923353DB3D6CD7301 /* SwapOrderStatus.graphql.swift in Sources */, - C791C5505DBB3036B9D5066D /* SwapOrderType.graphql.swift in Sources */, - 0C6FC49E0FC9BCB2DF5B627E /* TokenSortableField.graphql.swift in Sources */, - FD84AA379C4562598807843D /* TokenStandard.graphql.swift in Sources */, - 2E05037B51AF526A47701B09 /* TransactionDirection.graphql.swift in Sources */, - CCBC45FD4310D8153D859361 /* TransactionStatus.graphql.swift in Sources */, - 5D06BAB366D14A5AFE2355E8 /* TransactionType.graphql.swift in Sources */, - FBE634E2AEC7A62B89863366 /* ContractInput.graphql.swift in Sources */, - 3E338E14E33C721DAB708664 /* NftActivityFilterInput.graphql.swift in Sources */, - 50C89DFAF2DBC80D6343B164 /* NftAssetTraitInput.graphql.swift in Sources */, - D680C4844F2C5DACF5D0892E /* NftAssetsFilterInput.graphql.swift in Sources */, - AA3AE4E5C2AAC3F9460A3637 /* NftBalanceAssetInput.graphql.swift in Sources */, - C7ABAADA504107D152A52FD4 /* NftBalancesFilterInput.graphql.swift in Sources */, - 09E8C497051C37FE604FD40E /* OnRampTransactionsAuth.graphql.swift in Sources */, - C1FC1F8B4A2725A195F66D60 /* PortfolioValueModifier.graphql.swift in Sources */, - E091F379106E92D3181103CB /* IAmount.graphql.swift in Sources */, - EF05F69E61EA8A16C6A66D53 /* IContract.graphql.swift in Sources */, - CE1DF567D58943ACCBB8360C /* Amount.graphql.swift in Sources */, - 1CE49305114BF4792290DDC6 /* AmountChange.graphql.swift in Sources */, - F5B577CA1201CB6FF04A9A58 /* ApplicationContract.graphql.swift in Sources */, - 9A3E861F5D7B0F2CB0EFA7A6 /* AssetActivity.graphql.swift in Sources */, - 8ADDD2E2AB1FD9D1D9AF5627 /* BlockaidFees.graphql.swift in Sources */, - 03291E0DA448AF438F97EA5D /* DescriptionTranslations.graphql.swift in Sources */, - 4C187202229BDC4D8898E067 /* Dimensions.graphql.swift in Sources */, - 5C3958DA046B5A84FE90C1BD /* FeeData.graphql.swift in Sources */, - 0CEBEB8BE3AB95C3F584F6CE /* Image.graphql.swift in Sources */, - B377DB0418EA15695F208D9F /* NetworkFee.graphql.swift in Sources */, - BD59A9C8414D6A4BFE9C9A2E /* NftActivity.graphql.swift in Sources */, - B746C09DA19B4C7F9C700989 /* NftActivityConnection.graphql.swift in Sources */, - FE9CE5C3B5A456B249FC34C5 /* NftActivityEdge.graphql.swift in Sources */, - 1DC34AE3E11264475E8B9F62 /* NftApproval.graphql.swift in Sources */, - FC7C117CEA5EA63460E6A2C6 /* NftApproveForAll.graphql.swift in Sources */, - BD1FF76A8E50EFEA0720CC04 /* NftAsset.graphql.swift in Sources */, - 8CC06A8205186D0640F0BC55 /* NftAssetConnection.graphql.swift in Sources */, - 5804A1B1BB144D9EFBBE177D /* NftAssetEdge.graphql.swift in Sources */, - 126CC4BC99F3F15CC1E2F1C3 /* NftAssetTrait.graphql.swift in Sources */, - CF36F741187285B430EFE557 /* NftBalance.graphql.swift in Sources */, - 33928A7366AFE7F892CDC89F /* NftBalanceConnection.graphql.swift in Sources */, - CA0EDABCA1B6C2B0936BF5CF /* NftBalanceEdge.graphql.swift in Sources */, - 252BD40CB9B46FE47B2440B1 /* NftCollection.graphql.swift in Sources */, - D81BAFFCC105668B88B607FC /* NftCollectionConnection.graphql.swift in Sources */, - A3318C676D7FBF78A1583B16 /* NftCollectionEdge.graphql.swift in Sources */, - 85D0E81B798D0F2D841386E9 /* NftCollectionMarket.graphql.swift in Sources */, - 3FE25F715DFFD1D03DCDA57D /* NftContract.graphql.swift in Sources */, - 1B59968CF49C45B127E9C768 /* NftOrder.graphql.swift in Sources */, - 9B6C88F7D8D542AAC353A3EA /* NftOrderConnection.graphql.swift in Sources */, - 4BB9E218D89B8AF5E4E27CCB /* NftOrderEdge.graphql.swift in Sources */, - 6A60BDC9D46A710D871DEC6E /* NftProfile.graphql.swift in Sources */, - E54093DC857B8C634804D42B /* NftTransfer.graphql.swift in Sources */, - 0F282C26344F1AE3622232B0 /* OffRampTransactionDetails.graphql.swift in Sources */, - 1E2AF2C38C8FBEB2A95B644E /* OffRampTransfer.graphql.swift in Sources */, - CF2BAECCF9A2EC43AC3EC583 /* OnRampServiceProvider.graphql.swift in Sources */, - B61E182CF4937B5169885C95 /* OnRampTransactionDetails.graphql.swift in Sources */, - 553E6B467BEE49C9984691C5 /* OnRampTransfer.graphql.swift in Sources */, - 35B8176433A98BA798BBEE79 /* PageInfo.graphql.swift in Sources */, - 4FED6AAF896BA371C56D88B1 /* Portfolio.graphql.swift in Sources */, - 818179F5724AA35E1B1D5A9C /* ProtectionInfo.graphql.swift in Sources */, - ADE104A101B3DFFBDB308189 /* Query.graphql.swift in Sources */, - 03E3515E38E247F459218CAA /* SwapOrderDetails.graphql.swift in Sources */, - 31661D70B58410EA030E2C53 /* TimestampedAmount.graphql.swift in Sources */, - F2EB62621E64B57B07DE8B13 /* Token.graphql.swift in Sources */, - 9301D18644F3DFA9ABB8F0BE /* TokenApproval.graphql.swift in Sources */, - E24ED8688E9B18BBD543F8F0 /* TokenBalance.graphql.swift in Sources */, - AF83E7713BB625D787DD1A1D /* TokenMarket.graphql.swift in Sources */, - 0094F4FBBC1C0A2FFABF7157 /* TokenProject.graphql.swift in Sources */, - 869F3639FBC6D8156FFE3BD3 /* TokenProjectMarket.graphql.swift in Sources */, - 4EF8D293BB1EBCFBFC65A330 /* TokenTransfer.graphql.swift in Sources */, - 97CA219E1FFD832D8FA02C20 /* TransactionDetails.graphql.swift in Sources */, - BA83003638263D702D03C3C1 /* ActivityDetails.graphql.swift in Sources */, - 36E601F269D40A67FC353947 /* AssetChange.graphql.swift in Sources */, + 0D87DBFD19D5A6DF176C0AB7 /* MobileSchema.graphql.swift in Sources */, + A250CF455F6CEFB64ABFC277 /* HomeScreenTokenParts.graphql.swift in Sources */, + 0AC6A2E1C5837AAD902742DA /* TokenBalanceMainParts.graphql.swift in Sources */, + F77F1A182DCA1D8DFE46ACC8 /* TokenBalanceParts.graphql.swift in Sources */, + 02B7B534DFEC9DA0F7F06B8D /* TokenBalanceQuantityParts.graphql.swift in Sources */, + 6882C4768011FDD4D746BDDB /* TokenBasicInfoParts.graphql.swift in Sources */, + F09E3F15A36A9050B028B3E3 /* TokenBasicProjectParts.graphql.swift in Sources */, + 5551DDD6A8B9B28E03459816 /* TokenFeeDataParts.graphql.swift in Sources */, + 06C9F16E22B8B64855980A69 /* TokenMarketParts.graphql.swift in Sources */, + 2B2738BAB9E906B561E0D4D6 /* TokenParts.graphql.swift in Sources */, + 9D638326CD705ABE549C8CA7 /* TokenProjectMarketsParts.graphql.swift in Sources */, + FA318FB37223FAF04A5C887B /* TokenProjectUrlsParts.graphql.swift in Sources */, + 47797825D18637A2FE57AC1F /* TokenProtectionInfoParts.graphql.swift in Sources */, + CE8C365D91CADE7A5A8F4D52 /* TopTokenParts.graphql.swift in Sources */, + D6149AE9ED70F546DF5841BD /* ConvertQuery.graphql.swift in Sources */, + ED606CD83873CC9DFCCA44F1 /* FavoriteTokenCardQuery.graphql.swift in Sources */, + 147F4DFD43CE86324A066003 /* FeedTransactionListQuery.graphql.swift in Sources */, + 6250D4ACD5696D845FD83DFD /* HomeScreenTokensQuery.graphql.swift in Sources */, + CEE9E9912D5621F6E8F819B7 /* MultiplePortfolioBalancesQuery.graphql.swift in Sources */, + 70CD2D0665E5AC6A0DF6F697 /* NFTItemScreenQuery.graphql.swift in Sources */, + 206E0025B8FCE0EA96AC744A /* NftCollectionScreenQuery.graphql.swift in Sources */, + 7C886F9D4C02EF4E5F67B011 /* NftsQuery.graphql.swift in Sources */, + 092E5F0FE7DE6113285AF5E3 /* NftsTabQuery.graphql.swift in Sources */, + 57ECD348EA564F0CBF715E9E /* PortfolioBalancesQuery.graphql.swift in Sources */, + C0BBEE1CAEA4B2F778426BDE /* SelectWalletScreenQuery.graphql.swift in Sources */, + 81E55E8D3056E2378A332B31 /* TokenDetailsScreenQuery.graphql.swift in Sources */, + 34610491FF9A452F4F806157 /* TokenPriceHistoryQuery.graphql.swift in Sources */, + 1C84E14C7F52CF6C9A6D2930 /* TokenProjectDescriptionQuery.graphql.swift in Sources */, + DC4AA6DE28B9F40A3D7600F1 /* TokenProjectsQuery.graphql.swift in Sources */, + D0B9B6DEC559290B7F64B24F /* TokenQuery.graphql.swift in Sources */, + E654760CA0FF19139A85472A /* TokensQuery.graphql.swift in Sources */, + 8E6DA65AC5CAE9F9A67F9E38 /* TopTokensQuery.graphql.swift in Sources */, + FC61EDE606C85346CE069C5D /* TransactionHistoryUpdaterQuery.graphql.swift in Sources */, + E1009979B48ACF5017C12F09 /* TransactionListQuery.graphql.swift in Sources */, + EE4B861AE191A1BF70CF3784 /* WidgetTokensQuery.graphql.swift in Sources */, + AD6036D32C44048781B728E1 /* SchemaConfiguration.swift in Sources */, + 9EBC41A90B1A80653960045E /* SchemaMetadata.graphql.swift in Sources */, + F814C0144D90ADB4A8E0A34E /* Chain.graphql.swift in Sources */, + 9260D04A891FEDE2BF511707 /* Currency.graphql.swift in Sources */, + 23B195B262495EABE4A6CDF4 /* HistoryDuration.graphql.swift in Sources */, + D5F36D3EDC206DF15EC368AC /* NftActivityType.graphql.swift in Sources */, + 4DB6B64CFF3B00FCF0258336 /* NftMarketplace.graphql.swift in Sources */, + 91DBDA9B4F4006350AEB8E4B /* NftStandard.graphql.swift in Sources */, + F86D86FC31BBDA7246C50392 /* ProtectionAttackType.graphql.swift in Sources */, + CA966CBD02A7B16BC55F1B8E /* ProtectionResult.graphql.swift in Sources */, + 15193A0A3CE80FF72AAB54B4 /* SafetyLevel.graphql.swift in Sources */, + 3720F641F397A2819500B1B1 /* SwapOrderStatus.graphql.swift in Sources */, + E7D4A29333634717D099F80E /* SwapOrderType.graphql.swift in Sources */, + D48D1B7A4469BAAA22608058 /* TokenSortableField.graphql.swift in Sources */, + D1FB4E293EC152C12A495ACD /* TokenStandard.graphql.swift in Sources */, + 252A28057D1D481D14D2F5E5 /* TransactionDirection.graphql.swift in Sources */, + BFB114718A0D262E3AEAEDC0 /* TransactionStatus.graphql.swift in Sources */, + AAB837C01239A62D00068853 /* TransactionType.graphql.swift in Sources */, + 8BCFF1F648887F78894F1071 /* ContractInput.graphql.swift in Sources */, + C8F5AF75BDB439143FE1C173 /* NftActivityFilterInput.graphql.swift in Sources */, + 4FD78AA88D2EB8E22144BCDF /* NftAssetTraitInput.graphql.swift in Sources */, + 997BFD5324E4BDE2422FEFED /* NftAssetsFilterInput.graphql.swift in Sources */, + 0764D0BF05C44615BDD9AD65 /* NftBalanceAssetInput.graphql.swift in Sources */, + 51362EC6F0E6D8928C067F5E /* NftBalancesFilterInput.graphql.swift in Sources */, + BB28AF35DC102F8DE2A4BE9D /* OnRampTransactionsAuth.graphql.swift in Sources */, + 9EC9E4AB5C3FD254EDB28D7A /* PortfolioValueModifier.graphql.swift in Sources */, + A949039A6A9EB3584B5644F3 /* IAmount.graphql.swift in Sources */, + 2FC53D1C58218F0BF1D4E5F3 /* IContract.graphql.swift in Sources */, + 845327D60EFBB850189D6AB3 /* Amount.graphql.swift in Sources */, + ACA7AE75760B76F7B30DCD43 /* AmountChange.graphql.swift in Sources */, + 4DB88A2CBFF5FF356CA757F0 /* ApplicationContract.graphql.swift in Sources */, + 0AD9B4E176D2E9FE86E6ED21 /* AssetActivity.graphql.swift in Sources */, + 391FD815120230BDAF53F6F0 /* BlockaidFees.graphql.swift in Sources */, + 24B4011ADC672F470EC515AC /* BridgedWithdrawalInfo.graphql.swift in Sources */, + E2C528F617A9675690171D54 /* DescriptionTranslations.graphql.swift in Sources */, + BAB54489223FD0027F7A8DBE /* Dimensions.graphql.swift in Sources */, + 15A72E27235628C56431EC98 /* FeeData.graphql.swift in Sources */, + 86038259E487A108DDC2948B /* Image.graphql.swift in Sources */, + C064037906B259088B6B78B2 /* NetworkFee.graphql.swift in Sources */, + BE0DBD01CF4DCA6D3CDDA369 /* NftActivity.graphql.swift in Sources */, + 19BC2371F6BE6CAD5218AA53 /* NftActivityConnection.graphql.swift in Sources */, + 71954B20E4341CEC36203F87 /* NftActivityEdge.graphql.swift in Sources */, + 3389D45727F15B8E4F59B99F /* NftApproval.graphql.swift in Sources */, + 39DEA445993253BCC1201199 /* NftApproveForAll.graphql.swift in Sources */, + BED0DD22C0D9E09C85DF010D /* NftAsset.graphql.swift in Sources */, + E7B6F1CA0E30585C949C9D9A /* NftAssetConnection.graphql.swift in Sources */, + 7700BED7CD7F52C83A3FF430 /* NftAssetEdge.graphql.swift in Sources */, + D8A0C6D04FF53BA4F50543FE /* NftAssetTrait.graphql.swift in Sources */, + DB752E8F664505726ABDBCD3 /* NftBalance.graphql.swift in Sources */, + B64BD6DEB100AEB723DB640D /* NftBalanceConnection.graphql.swift in Sources */, + E475BA358DD8BE30A6FDC051 /* NftBalanceEdge.graphql.swift in Sources */, + 1CA252C75CD5291E2A0B308B /* NftCollection.graphql.swift in Sources */, + 8141B38CCDB09F91180C0EBD /* NftCollectionConnection.graphql.swift in Sources */, + 5676CCD265609B71D3B1DB7C /* NftCollectionEdge.graphql.swift in Sources */, + 31A4EC91F1924E25AECA2F4E /* NftCollectionMarket.graphql.swift in Sources */, + 438115E2759B1194751A8021 /* NftContract.graphql.swift in Sources */, + E7EDBB8CDF65D5D6602BF8FC /* NftOrder.graphql.swift in Sources */, + A9AF7B483E9666E88CD6253E /* NftOrderConnection.graphql.swift in Sources */, + 9E7EC26AC45301198E280DC7 /* NftOrderEdge.graphql.swift in Sources */, + 9061ACE5AF99CEDE7BA51C94 /* NftProfile.graphql.swift in Sources */, + EB0A75424F8EEF6612D28D52 /* NftTransfer.graphql.swift in Sources */, + 9A9B39BC22F3BDFCD0917B66 /* OffRampTransactionDetails.graphql.swift in Sources */, + F1193089AE71CA3C4C101EA5 /* OffRampTransfer.graphql.swift in Sources */, + A88A26642AC25B022F428953 /* OnRampServiceProvider.graphql.swift in Sources */, + 03EEEDA1A3D5EF23C0D14B08 /* OnRampTransactionDetails.graphql.swift in Sources */, + 4260B9F719F8E25DFBF99D73 /* OnRampTransfer.graphql.swift in Sources */, + 7E3412EB776E1F43D6904BA0 /* PageInfo.graphql.swift in Sources */, + 7B557C3224F990851430DBD2 /* Portfolio.graphql.swift in Sources */, + 2C9935142A9582467B5B5FC5 /* ProtectionInfo.graphql.swift in Sources */, + CBEB9122D993BCB0E9A604B7 /* Query.graphql.swift in Sources */, + 03AE019583139330A3E408AB /* SwapOrderDetails.graphql.swift in Sources */, + 16506815CDEE17670D3DD363 /* TimestampedAmount.graphql.swift in Sources */, + F49C1C4E9175DFE7EEF9FB51 /* Token.graphql.swift in Sources */, + 2D73D2D80BC8C455DB35CE57 /* TokenApproval.graphql.swift in Sources */, + 8117BFB02DCF0F315FD83E67 /* TokenBalance.graphql.swift in Sources */, + E4985BA9090E013F2C9FC190 /* TokenMarket.graphql.swift in Sources */, + 8971E73E041A6283E2684481 /* TokenProject.graphql.swift in Sources */, + 4DB6D0FB611F6C68EADFB948 /* TokenProjectMarket.graphql.swift in Sources */, + 15F6E43DA2BD9A8A681EC70C /* TokenTransfer.graphql.swift in Sources */, + 2925C6E0262B95C5EA0C2AE7 /* TransactionDetails.graphql.swift in Sources */, + 63FBF9C23ED568816590F093 /* ActivityDetails.graphql.swift in Sources */, + 8D85F349FEF81A52FB93EAAB /* AssetChange.graphql.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2796,11 +2807,12 @@ 8E89C3B22AB8AAA400C84DE5 /* MnemonicTextField.swift in Sources */, FD7304D028A3650A0085BDEA /* Colors.swift in Sources */, 8E89C3AF2AB8AAA400C84DE5 /* MnemonicDisplayView.swift in Sources */, - 9FEC9B8B2A858CF1003CD019 /* AppDelegate.m in Sources */, + 45FFF7E12E8C2E6900362570 /* SilentPushEventEmitter.swift in Sources */, 6BC7D0802B5FF02400617C95 /* EncryptionUtils.swift in Sources */, 03C788232C10E7390011E5DC /* ActionButtons.swift in Sources */, 8EA8AB3B2AB7ED3C004E7EF3 /* SeedPhraseInputManager.m in Sources */, 03D2F3182C218D390030D987 /* RelativeOffsetView.swift in Sources */, + 45FFF7DF2E8C2A8100362570 /* SilentPushEventEmitter.m in Sources */, 6CA91BDB2A95223C00C4063E /* RNEthersRS.swift in Sources */, 8EA8AB3C2AB7ED3C004E7EF3 /* SeedPhraseInputViewModel.swift in Sources */, 072F6C2E2A44A32F00DA720A /* TokenPriceWidget.intentdefinition in Sources */, @@ -2810,6 +2822,7 @@ 6BC7D07F2B5FF02400617C95 /* ScantasticEncryption.swift in Sources */, 07B0676C2A7D6EC8001DD9B9 /* RNWidgets.swift in Sources */, 8E89C3AE2AB8AAA400C84DE5 /* MnemonicConfirmationView.swift in Sources */, + 8B2A92172EB3E78E00990413 /* AppDelegate.swift in Sources */, 5B4398EC2DD3B22C00F6BE08 /* PrivateKeyDisplayManager.m in Sources */, 5B4398ED2DD3B22C00F6BE08 /* PrivateKeyDisplayManager.swift in Sources */, 5B4398EE2DD3B22C00F6BE08 /* PrivateKeyDisplayView.swift in Sources */, @@ -2819,7 +2832,6 @@ 649A7A782D9AE70B00B53589 /* KeychainUtils.swift in Sources */, 649A7A792D9AE70B00B53589 /* KeychainConstants.swift in Sources */, 9FCEBF002A95A8E00079EDDB /* RNWalletConnect.m in Sources */, - 13B07FC11A68108700A75B9A /* main.m in Sources */, 6CA91BE32A95226200C4063E /* RNCloudStorageBackupsManager.swift in Sources */, 9FCEBF042A95A99C0079EDDB /* RCTThemeModule.m in Sources */, 9FCEBF012A95A8E00079EDDB /* RNWalletConnect.swift in Sources */, @@ -2985,7 +2997,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.64.1; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; @@ -3038,7 +3050,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.64.1; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore; @@ -3091,7 +3103,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.64.1; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore; @@ -3144,7 +3156,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.64.1; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore; @@ -3182,7 +3194,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.1; - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.64.1; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; @@ -3218,7 +3230,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.1; - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.64.1; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests; @@ -3253,7 +3265,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.1; - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.64.1; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests; @@ -3288,7 +3300,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.1; - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.64.1; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests; @@ -3335,7 +3347,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.64.1; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; @@ -3381,7 +3393,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.64.1; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.widgets; @@ -3427,7 +3439,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.64.1; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.widgets; @@ -3473,7 +3485,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.64.1; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.widgets; @@ -3515,7 +3527,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.64.1; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; @@ -3558,7 +3570,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.64.1; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.WidgetIntentExtension; @@ -3601,7 +3613,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.64.1; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.WidgetIntentExtension; @@ -3644,7 +3656,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.64.1; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.WidgetIntentExtension; @@ -3664,7 +3676,7 @@ baseConfigurationReference = A7C9F415D0E128A43003E071 /* Pods-Uniswap.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; - ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon${BUNDLE_ID_SUFFIX}"; + ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon$(BUNDLE_ID_SUFFIX)"; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = ""; BUNDLE_ID_SUFFIX = .dev; CLANG_ENABLE_MODULES = YES; @@ -3680,14 +3692,14 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.64.1; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", "-lc++", ); OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; - PRODUCT_BUNDLE_IDENTIFIER = "com.uniswap.mobile${BUNDLE_ID_SUFFIX}"; + PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev; PRODUCT_NAME = Uniswap; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Uniswap/RNEthersRs/RNEthersRS-Bridging-Header.h"; @@ -3702,7 +3714,7 @@ baseConfigurationReference = 178644A78AB62609EFDB66B3 /* Pods-Uniswap.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; - ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon${BUNDLE_ID_SUFFIX}"; + ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon$(BUNDLE_ID_SUFFIX)"; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = ""; BUNDLE_ID_SUFFIX = ""; CLANG_ENABLE_MODULES = YES; @@ -3718,14 +3730,14 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.64.1; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", "-lc++", ); OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; - PRODUCT_BUNDLE_IDENTIFIER = "com.uniswap.mobile${BUNDLE_ID_SUFFIX}"; + PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile; PRODUCT_NAME = Uniswap; PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.uniswap.mobile"; SWIFT_OBJC_BRIDGING_HEADER = "Uniswap/RNEthersRs/RNEthersRS-Bridging-Header.h"; @@ -3813,6 +3825,7 @@ REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native"; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; + SWIFT_VERSION = 5.0; USE_HERMES = true; }; name = Debug; @@ -3887,6 +3900,7 @@ OTHER_LDFLAGS = "$(inherited)"; REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native"; SDKROOT = iphoneos; + SWIFT_VERSION = 5.0; USE_HERMES = true; VALIDATE_PRODUCT = YES; }; @@ -3920,7 +3934,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.64.1; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; @@ -3965,7 +3979,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.64.1; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.OneSignalNotificationServiceExtension; @@ -4050,6 +4064,7 @@ OTHER_LDFLAGS = "$(inherited)"; REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native"; SDKROOT = iphoneos; + SWIFT_VERSION = 5.0; USE_HERMES = true; VALIDATE_PRODUCT = YES; }; @@ -4060,7 +4075,7 @@ baseConfigurationReference = 62CEA9F2D5176D20A6402A3E /* Pods-Uniswap.beta.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; - ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon${BUNDLE_ID_SUFFIX}"; + ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon$(BUNDLE_ID_SUFFIX)"; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = ""; BUNDLE_ID_SUFFIX = .beta; CLANG_ENABLE_MODULES = YES; @@ -4076,14 +4091,14 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.64.1; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", "-lc++", ); OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; - PRODUCT_BUNDLE_IDENTIFIER = "com.uniswap.mobile${BUNDLE_ID_SUFFIX}"; + PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta; PRODUCT_NAME = Uniswap; PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.uniswap.mobile.beta"; SWIFT_OBJC_BRIDGING_HEADER = "Uniswap/RNEthersRs/RNEthersRS-Bridging-Header.h"; @@ -4148,7 +4163,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.64.1; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.OneSignalNotificationServiceExtension; @@ -4233,6 +4248,7 @@ OTHER_LDFLAGS = "$(inherited)"; REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native"; SDKROOT = iphoneos; + SWIFT_VERSION = 5.0; USE_HERMES = true; VALIDATE_PRODUCT = YES; }; @@ -4243,7 +4259,7 @@ baseConfigurationReference = 56FE9C9AF785221B7E3F4C04 /* Pods-Uniswap.dev.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; - ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon${BUNDLE_ID_SUFFIX}"; + ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon$(BUNDLE_ID_SUFFIX)"; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = ""; BUNDLE_ID_SUFFIX = .dev; CLANG_ENABLE_MODULES = YES; @@ -4259,14 +4275,14 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.64.1; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", "-lc++", ); OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; - PRODUCT_BUNDLE_IDENTIFIER = "com.uniswap.mobile${BUNDLE_ID_SUFFIX}"; + PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev; PRODUCT_NAME = Uniswap; PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.uniswap.mobile.dev"; SWIFT_OBJC_BRIDGING_HEADER = "Uniswap/RNEthersRs/RNEthersRS-Bridging-Header.h"; @@ -4331,7 +4347,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.64.1; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.OneSignalNotificationServiceExtension; diff --git a/apps/mobile/ios/Uniswap/AppDelegate.h b/apps/mobile/ios/Uniswap/AppDelegate.h deleted file mode 100644 index 3151f6de6ed..00000000000 --- a/apps/mobile/ios/Uniswap/AppDelegate.h +++ /dev/null @@ -1,7 +0,0 @@ -#import -#import -#import - -@interface AppDelegate : RCTAppDelegate - -@end diff --git a/apps/mobile/ios/Uniswap/AppDelegate.m b/apps/mobile/ios/Uniswap/AppDelegate.m deleted file mode 100644 index 28b0f7e1310..00000000000 --- a/apps/mobile/ios/Uniswap/AppDelegate.m +++ /dev/null @@ -1,131 +0,0 @@ -#import "AppDelegate.h" - -#import - -#import "Uniswap-Swift.h" - -#import -#import -#import -#import -#import - -@implementation AppDelegate - -static NSString *const hasLaunchedOnceKey = @"HasLaunchedOnce"; - -/** - * Handles keychain cleanup on first run of the app. - * A migration flag is persisted in the keychain to avoid clearing the keychain for existing users, while the first run flag is saved in NSUserDefaults, which is cleared every install. - */ -- (void)handleKeychainCleanup { - NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; - BOOL isFirstRun = ![defaults boolForKey:hasLaunchedOnceKey]; - BOOL canClearKeychainOnReinstall = [KeychainUtils getCanClearKeychainOnReinstall]; - - if (canClearKeychainOnReinstall && isFirstRun) { - [KeychainUtils clearKeychain]; - } - - if (!canClearKeychainOnReinstall || isFirstRun) { - [defaults setBool:YES forKey:hasLaunchedOnceKey]; - [KeychainUtils setCanClearKeychainOnReinstall]; - } -} - -- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions -{ - // Must be first line in startup routine - [ReactNativePerformance onAppStarted]; - - [self handleKeychainCleanup]; - - [FIRApp configure]; - - // This is needed so universal links opened from OneSignal notifications navigate to the proper page. - // More details here: - // https://documentation.onesignal.com/v7.0/docs/react-native-sdk in the deep linking warning section. - NSMutableDictionary *newLaunchOptions = [NSMutableDictionary dictionaryWithDictionary:launchOptions]; - if (launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]) { - NSDictionary *remoteNotif = launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]; - if (remoteNotif[@"custom"] && remoteNotif[@"custom"][@"u"]) { - NSString *initialURL = remoteNotif[@"custom"][@"u"]; - if (!launchOptions[UIApplicationLaunchOptionsURLKey]) { - newLaunchOptions[UIApplicationLaunchOptionsURLKey] = [NSURL URLWithString:initialURL]; - } - } - } - - self.moduleName = @"Uniswap"; - self.dependencyProvider = [RCTAppDependencyProvider new]; - self.initialProps = @{}; - - [self.window makeKeyAndVisible]; - - if (@available(iOS 13.0, *)) { - self.window.rootViewController.view.backgroundColor = [UIColor systemBackgroundColor]; - } else { - self.window.rootViewController.view.backgroundColor = [UIColor whiteColor]; - } - - [super application:application didFinishLaunchingWithOptions:newLaunchOptions]; - - [[RCTI18nUtil sharedInstance] allowRTL:NO]; - [RNBootSplash initWithStoryboard:@"SplashScreen" rootView:self.window.rootViewController.view]; - - return YES; -} - -- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge -{ - return [self bundleURL]; -} - -- (NSURL *)bundleURL -{ -#if DEBUG - return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"]; -#else - return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; -#endif -} - -/// This method controls whether the `concurrentRoot`feature of React18 is turned on or off. -/// -/// @see: https://reactjs.org/blog/2022/03/29/react-v18.html -/// @note: This requires to be rendering on Fabric (i.e. on the New Architecture). -/// @return: `true` if the `concurrentRoot` feature is enabled. Otherwise, it returns `false`. -- (BOOL)concurrentRootEnabled -{ - return true; -} - -// Enable deep linking -- (BOOL)application:(UIApplication *)application - openURL:(NSURL *)url - options:(NSDictionary *)options -{ - return [RCTLinkingManager application:application openURL:url options:options]; -} - -// Enable universal links -- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity - restorationHandler:(nonnull void (^)(NSArray> * _Nullable))restorationHandler -{ - return [RCTLinkingManager application:application - continueUserActivity:userActivity - restorationHandler:restorationHandler]; -} - -// Disable 3rd party keyboard --(BOOL)application:(UIApplication *)application shouldAllowExtensionPointIdentifier:(NSString *)extensionPointIdentifier -{ - if (extensionPointIdentifier == UIApplicationKeyboardExtensionPointIdentifier) - { - return NO; - } - - return YES; -} - -@end diff --git a/apps/mobile/ios/Uniswap/AppDelegate.swift b/apps/mobile/ios/Uniswap/AppDelegate.swift new file mode 100644 index 00000000000..ca105e69da3 --- /dev/null +++ b/apps/mobile/ios/Uniswap/AppDelegate.swift @@ -0,0 +1,163 @@ +import UIKit +import Expo +import ExpoModulesCore +import React +import ReactAppDependencyProvider +import Firebase +import ReactNativePerformance +import RNBootSplash +import UserNotifications + +@main +class AppDelegate: ExpoAppDelegate { + + static let hasLaunchedOnceKey = "HasLaunchedOnce" + + var window: UIWindow? + var reactNativeDelegate: ExpoReactNativeFactoryDelegate? + var reactNativeFactory: ExpoReactNativeFactory? + + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { + print("🚀 AppDelegate: Starting initialization") + + // Must be first line in startup routine + ReactNativePerformance.onAppStarted() + print("📊 ReactNativePerformance started") + + // Handle keychain cleanup on first launch + handleKeychainCleanup() + print("🔐 Keychain cleanup completed") + + // Configure Firebase + FirebaseApp.configure() + print("🔥 Firebase configured") + + // Handle OneSignal deep linking + var newLaunchOptions = launchOptions ?? [:] + if let remoteNotif = launchOptions?[UIApplication.LaunchOptionsKey.remoteNotification] as? [String: Any], + let custom = remoteNotif["custom"] as? [String: Any], + let initialURL = custom["u"] as? String, + launchOptions?[UIApplication.LaunchOptionsKey.url] == nil { + newLaunchOptions[UIApplication.LaunchOptionsKey.url] = URL(string: initialURL) + print("🔗 OneSignal deep link processed") + } + + // Set up Expo React Native factory + let delegate = ReactNativeDelegate() + let factory = ExpoReactNativeFactory(delegate: delegate) + delegate.dependencyProvider = RCTAppDependencyProvider() + + reactNativeDelegate = delegate + reactNativeFactory = factory + bindReactNativeFactory(factory) + + window = UIWindow(frame: UIScreen.main.bounds) + factory.startReactNative( + withModuleName: "Uniswap", + in: window, + launchOptions: newLaunchOptions + ) + + let result = super.application(application, didFinishLaunchingWithOptions: newLaunchOptions) + + print("🏁 AppDelegate initialization complete") + return result + } + + // MARK: - Keychain Cleanup + private func handleKeychainCleanup() { + let defaults = UserDefaults.standard + let isFirstRun = !defaults.bool(forKey: AppDelegate.hasLaunchedOnceKey) + let canClearKeychainOnReinstall = KeychainUtils.getCanClearKeychainOnReinstall() + + if canClearKeychainOnReinstall && isFirstRun { + KeychainUtils.clearKeychain() + } + + if !canClearKeychainOnReinstall || isFirstRun { + defaults.set(true, forKey: AppDelegate.hasLaunchedOnceKey) + KeychainUtils.setCanClearKeychainOnReinstall() + } + } + + // MARK: - Deep Linking + override func application( + _ app: UIApplication, + open url: URL, + options: [UIApplication.OpenURLOptionsKey: Any] = [:] + ) -> Bool { + return super.application(app, open: url, options: options) || RCTLinkingManager.application(app, open: url, options: options) + } + + // Universal Links + override func application( + _ application: UIApplication, + continue userActivity: NSUserActivity, + restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void + ) -> Bool { + let result = RCTLinkingManager.application(application, continue: userActivity, restorationHandler: restorationHandler) + return super.application(application, continue: userActivity, restorationHandler: restorationHandler) || result + } + + // MARK: - Push Notifications + override func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { + // Handle device token registration + // OneSignal and other services will handle this via swizzling + } + + override func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { + // Handle registration failure + print("Failed to register for remote notifications: \(error)") + } + + override func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { + if let aps = userInfo["aps"] as? [String: Any] { + let contentAvailable = aps["content-available"] ?? aps["content_available"] + + if let contentNumber = contentAvailable as? NSNumber, contentNumber.intValue == 1 { + // Convert obj-c payload to SilentPushEventEmitter + let payload = userInfo.reduce(into: [String: Any]()) { result, entry in + if let key = entry.key as? String { + result[key] = entry.value + } + } + + SilentPushEventEmitter.emitEvent(with: payload) + } + } + completionHandler(.noData) + } + + // MARK: - Security + @objc(application:shouldAllowExtensionPointIdentifier:) + func application(_ application: UIApplication, shouldAllowExtensionPointIdentifier extensionPointIdentifier: UIApplication.ExtensionPointIdentifier) -> Bool { + // Disable 3rd party keyboards + if extensionPointIdentifier == .keyboard { + return false + } + return true + } +} + +class ReactNativeDelegate: ExpoReactNativeFactoryDelegate { + override func sourceURL(for bridge: RCTBridge) -> URL? { + bridge.bundleURL ?? bundleURL() + } + + override func bundleURL() -> URL? { + #if DEBUG + return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: ".expo/.virtual-metro-entry") + #else + return Bundle.main.url(forResource: "main", withExtension: "jsbundle") + #endif + } + + // Override customize to initialize RNBootSplash BEFORE the window becomes visible + public override func customize(_ rootView: UIView) { + super.customize(rootView) + RNBootSplash.initWithStoryboard("SplashScreen", rootView: rootView) + } +} diff --git a/apps/mobile/ios/Uniswap/Notifications/SilentPushEventEmitter.m b/apps/mobile/ios/Uniswap/Notifications/SilentPushEventEmitter.m new file mode 100644 index 00000000000..343b5119cf9 --- /dev/null +++ b/apps/mobile/ios/Uniswap/Notifications/SilentPushEventEmitter.m @@ -0,0 +1,18 @@ +// +// SilentPushEventEmitter.m +// Uniswap +// +// Created by John Short on 9/29/25. +// + +#import +#import +#import + +@interface RCT_EXTERN_MODULE(SilentPushEventEmitter, RCTEventEmitter) + +RCT_EXTERN_METHOD(supportedEvents) +RCT_EXTERN_METHOD(addListener:(NSString *)eventName) +RCT_EXTERN_METHOD(removeListeners:(nonnull NSNumber *)count) + +@end diff --git a/apps/mobile/ios/Uniswap/Notifications/SilentPushEventEmitter.swift b/apps/mobile/ios/Uniswap/Notifications/SilentPushEventEmitter.swift new file mode 100644 index 00000000000..c79a3fb051a --- /dev/null +++ b/apps/mobile/ios/Uniswap/Notifications/SilentPushEventEmitter.swift @@ -0,0 +1,31 @@ +// +// SilentPushEventEmitter.swift +// Uniswap +// +// Created by John Short on 9/29/25. +// + +import React + +@objc(SilentPushEventEmitter) +open class SilentPushEventEmitter: RCTEventEmitter { + + public static weak var emitter: RCTEventEmitter? + + override init() { + super.init() + SilentPushEventEmitter.emitter = self + } + + open override func supportedEvents() -> [String] { + ["SilentPushReceived"] + } + + @objc(emitEventWithPayload:) + public static func emitEvent(with payload: [String: Any]) { + guard let emitter = emitter else { + return + } + emitter.sendEvent(withName: "SilentPushReceived", body: payload) + } +} diff --git a/apps/mobile/ios/Uniswap/Uniswap-Bridging-Header.h b/apps/mobile/ios/Uniswap/Uniswap-Bridging-Header.h new file mode 100644 index 00000000000..8a24474f8ac --- /dev/null +++ b/apps/mobile/ios/Uniswap/Uniswap-Bridging-Header.h @@ -0,0 +1,23 @@ +// +// Uniswap-Bridging-Header.h +// Uniswap +// +// Bridging header for Swift/Objective-C interoperability +// + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import "libethers_ffi.h" + +// Import any other Objective-C headers that need to be accessible from Swift \ No newline at end of file diff --git a/apps/mobile/ios/Uniswap/main.m b/apps/mobile/ios/Uniswap/main.m deleted file mode 100644 index b1df44b953e..00000000000 --- a/apps/mobile/ios/Uniswap/main.m +++ /dev/null @@ -1,9 +0,0 @@ -#import - -#import "AppDelegate.h" - -int main(int argc, char * argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} diff --git a/apps/mobile/ios/WidgetsCore/.mobileschema_fingerprint b/apps/mobile/ios/WidgetsCore/.mobileschema_fingerprint index dcdbb6388ae..bdbfc2b1a8b 100644 --- a/apps/mobile/ios/WidgetsCore/.mobileschema_fingerprint +++ b/apps/mobile/ios/WidgetsCore/.mobileschema_fingerprint @@ -1 +1 @@ -7a03144e3423ea77bd883f7e79a0e3d6a3c54ea3e98424ac288e5f817f88931c \ No newline at end of file +6ac99a17ab7586621527349b19aa9fee726de2187db2a35e940d94747d2b4cf8 \ No newline at end of file diff --git a/apps/mobile/ios/sourcemaps-datadog.sh b/apps/mobile/ios/sourcemaps-datadog.sh index b182975a380..0bf96a9cb4a 100755 --- a/apps/mobile/ios/sourcemaps-datadog.sh +++ b/apps/mobile/ios/sourcemaps-datadog.sh @@ -1,6 +1,64 @@ #!/bin/sh +# Note: Not using 'set -e' because we want to handle errors gracefully -REACT_NATIVE_XCODE="../../../node_modules/react-native/scripts/react-native-xcode.sh" -DATADOG_XCODE="../../../node_modules/.bin/datadog-ci react-native xcode" +# Fix invalid paths for --entry-file and --assets-dest params, +# needed for react-native/scripts/bundle.js script. +export ENTRY_FILE="apps/mobile/index.js" +export DEST="ios/Uniswap.app" -/bin/sh -c "$DATADOG_XCODE $REACT_NATIVE_XCODE" +# Store the starting directory, and if we're in an `ios` dir, move up to parent +START_DIR=$(pwd) +BASENAME=$(basename "$START_DIR") +if [ "$BASENAME" = "ios" ]; then + cd .. +fi + +DATADOG_XCODE="../../node_modules/.bin/datadog-ci react-native xcode" +REACT_NATIVE_XCODE="../../node_modules/react-native/scripts/react-native-xcode.sh" + +# Create a temporary file for capturing output. +TEMP_LOG=$(mktemp) + +# As Xcode doesn't show echo messages by default, we enforce printing logs with the warning label. +echo "warning: Starting Datadog source map generation and upload..." +echo "warning: Command: $DATADOG_XCODE $REACT_NATIVE_XCODE" +echo "warning: SOURCEMAP_FILE: $SOURCEMAP_FILE" +echo "warning: Configuration: $CONFIGURATION" +echo "" + +# Run the datadog-ci command and capture both stdout and stderr +# Use pipefail to catch the exit code of the datadog command, not tee +set -o pipefail +if /bin/sh -c "$DATADOG_XCODE $REACT_NATIVE_XCODE" 2>&1 | tee "$TEMP_LOG"; then + set +o pipefail + echo "warning: Datadog source map upload completed successfully" + rm -f "$TEMP_LOG" + exit 0 +else + set +o pipefail + EXIT_CODE=$? + echo "error: " + echo "error: Datadog Source Map Upload Failed" + echo "error: Exit Code: $EXIT_CODE" + echo "error: " + echo "error: Full Error Output:" + echo "error: ---" + echo "error: $(cat "$TEMP_LOG")" + echo "error: ---" + echo "error: " + echo "error: Debug Information:" + echo "error: - datadog-ci version: $(../../../node_modules/.bin/datadog-ci version 2>&1 || echo 'Failed to get version')" + echo "error: - Node version: $(node --version 2>&1 || echo 'Node not found')" + echo "error: - React Native CLI: $(../../../node_modules/.bin/react-native --version 2>&1 || echo 'RN CLI not found')" + echo "error: - Working directory: $(pwd)" + echo "error: - DATADOG_API_KEY set: $([ -n "$DATADOG_API_KEY" ] && echo 'Yes' || echo 'No')" + echo "error: - Bundle file exists: $([ -f "$CONFIGURATION_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH/main.jsbundle" ] && echo 'Yes' || echo 'No')" + echo "error: - Source map exists: $([ -f "$SOURCEMAP_FILE" ] && echo "Yes ($SOURCEMAP_FILE)" || echo "No ($SOURCEMAP_FILE)")" + echo "error: " + echo "error: This is non-critical. Build will continue." + echo "error: Please report this error for investigation." + + rm -f "$TEMP_LOG" + # Exit with 0 to not fail the build + exit 0 +fi diff --git a/apps/mobile/jest-setup.js b/apps/mobile/jest-setup.js index 979c7cc8709..cb00bb1d297 100644 --- a/apps/mobile/jest-setup.js +++ b/apps/mobile/jest-setup.js @@ -1,6 +1,7 @@ -// Setups and mocks can go here -// For example: https://reactnavigation.org/docs/testing/ - +// From https://reactnavigation.org/docs/testing/#setting-up-jest +import 'react-native-gesture-handler/jestSetup'; +import { setUpTests } from 'react-native-reanimated'; +// Other import 'core-js' // necessary so setImmediate works in tests import 'utilities/jest-package-mocks' import 'uniswap/jest-package-mocks' @@ -11,6 +12,11 @@ import 'uniswap/src/i18n' // Uses real translations for tests import mockRNCNetInfo from '@react-native-community/netinfo/jest/netinfo-mock.js' +setUpTests() + +// Silence the warning: Animated: `useNativeDriver` is not supported because the native animated module is missing +jest.mock('react-native/Libraries/Animated/NativeAnimatedModule'); + jest.mock('@uniswap/client-explore/dist/uniswap/explore/v1/service-ExploreStatsService_connectquery', () => {}) jest.mock('@walletconnect/react-native-compat', () => ({})) @@ -65,6 +71,22 @@ jest.mock('@react-native-community/netinfo', () => ({ ...mockRNCNetInfo, NetInfo jest.mock('react-native', () => { const RN = jest.requireActual('react-native') // use original implementation, which comes with mocks out of the box + // Mock Linking module within React Native + RN.Linking = { + openURL: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + canOpenURL: jest.fn(), + getInitialURL: jest.fn(), + } + + // Mock Share module within React Native + RN.Share = { + share: jest.fn(), + sharedAction: 'sharedAction', + dismissedAction: 'dismissedAction', + } + return RN }) @@ -74,22 +96,10 @@ jest.mock('@react-navigation/elements', () => ({ require('react-native-reanimated').setUpTests() -jest.mock('react-native/Libraries/Share/Share', () => ({ - share: jest.fn(), -})) - jest.mock('@react-native-firebase/auth', () => () => ({ signInAnonymously: jest.fn(), })) -jest.mock('react-native/Libraries/Linking/Linking', () => ({ - openURL: jest.fn(), - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - canOpenURL: jest.fn(), - getInitialURL: jest.fn(), -})) - jest.mock("react-native-bootsplash", () => { return { hide: jest.fn().mockResolvedValue(), diff --git a/apps/mobile/jest.config.js b/apps/mobile/jest.config.js index 6b32f9ca40d..6f5da1044a5 100644 --- a/apps/mobile/jest.config.js +++ b/apps/mobile/jest.config.js @@ -23,6 +23,5 @@ module.exports = { setupFiles: [ '../../config/jest-presets/jest/setup.js', './jest-setup.js', - '../../node_modules/react-native-gesture-handler/jestSetup.js', ], } diff --git a/apps/mobile/metro.config.js b/apps/mobile/metro.config.js index 26513961f50..d3215f0097a 100644 --- a/apps/mobile/metro.config.js +++ b/apps/mobile/metro.config.js @@ -1,61 +1,38 @@ -/** - * Metro configuration for React Native with support for SVG files - * https://github.com/react-native-svg/react-native-svg#use-with-svg-files - * - * @format - */ -const { getMetroAndroidAssetsResolutionFix } = require('react-native-monorepo-tools') -const androidAssetsResolutionFix = getMetroAndroidAssetsResolutionFix() +const withStorybook = require('@storybook/react-native/metro/withStorybook'); +const { mergeConfig } = require('@react-native/metro-config'); +const { getDefaultConfig: getExpoDefaultConfig } = require('expo/metro-config'); -const withStorybook = require('@storybook/react-native/metro/withStorybook') - -const path = require('path') -const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config') - -const mobileRoot = path.resolve(__dirname) -const workspaceRoot = path.resolve(mobileRoot, '../..') - -const watchFolders = [mobileRoot, `${workspaceRoot}/node_modules`, `${workspaceRoot}/packages`] - - -const defaultConfig = getDefaultConfig(__dirname) +const defaultConfig = getExpoDefaultConfig(__dirname); const { resolver: { sourceExts, assetExts }, -} = defaultConfig +} = defaultConfig; -const config = { +// Only customize necessary fields for SVG and Storybook support +const customConfig = { resolver: { - nodeModulesPaths: [`${workspaceRoot}/node_modules`], assetExts: assetExts.filter((ext) => ext !== 'svg'), sourceExts: [...sourceExts, 'svg', 'cjs'], }, transformer: { + babelTransformerPath: require.resolve('react-native-svg-transformer'), getTransformOptions: async () => ({ transform: { experimentalImportSupport: false, inlineRequires: true, }, }), - babelTransformerPath: require.resolve('react-native-svg-transformer'), - publicPath: androidAssetsResolutionFix.publicPath, - }, - server: { - enhanceMiddleware: (middleware) => { - return androidAssetsResolutionFix.applyMiddleware(middleware) - }, }, - watchFolders, -} - -const IS_STORYBOOK_ENABLED = process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test' - -// Checkout more useful options in the docs: https://github.com/storybookjs/react-native?tab=readme-ov-file#options -module.exports = withStorybook(mergeConfig(defaultConfig, config), { - // Set to false to remove storybook specific options - // you can also use a env variable to set this - enabled: IS_STORYBOOK_ENABLED, - onDisabledRemoveStorybook: true, - // Path to your storybook config - configPath: path.resolve(__dirname, './.storybook'), -}) +}; + +const IS_STORYBOOK_ENABLED = + process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test'; + +module.exports = withStorybook( + mergeConfig(getExpoDefaultConfig(__dirname), defaultConfig, customConfig), + { + enabled: IS_STORYBOOK_ENABLED, + onDisabledRemoveStorybook: true, + configPath: require('path').resolve(__dirname, './.storybook'), + } +); diff --git a/apps/mobile/project.json b/apps/mobile/project.json index 64583483260..78f2b753000 100644 --- a/apps/mobile/project.json +++ b/apps/mobile/project.json @@ -4,37 +4,37 @@ "executor": "nx:noop" }, "android": { - "command": "rnef run:android --variant=devDebug --app-id-suffix=dev && bun run start", + "command": "EXPO_ANDROID_LAUNCH_ACTIVITY='com.uniswap.mobile.dev/com.uniswap.MainActivity' bunx expo run:android --variant=devDebug --app-id=com.uniswap.mobile.dev", "options": { "cwd": "{projectRoot}" } }, "android:release": { - "command": "rnef run:android --variant=devRelease --app-id-suffix=dev", + "command": "EXPO_ANDROID_LAUNCH_ACTIVITY='com.uniswap.mobile.dev/com.uniswap.MainActivity' bunx expo run:android --variant=devRelease --app-id=com.uniswap.mobile.dev", "options": { "cwd": "{projectRoot}" } }, "android:beta": { - "command": "rnef run:android --variant=betaDebug --app-id-suffix=beta", + "command": "EXPO_ANDROID_LAUNCH_ACTIVITY='com.uniswap.mobile.beta/com.uniswap.MainActivity' bunx expo run:android --variant=betaDebug --app-id=com.uniswap.mobile.beta", "options": { "cwd": "{projectRoot}" } }, "android:beta:release": { - "command": "rnef run:android --variant=betaRelease --app-id-suffix=beta", + "command": "EXPO_ANDROID_LAUNCH_ACTIVITY='com.uniswap.mobile.beta/com.uniswap.MainActivity' bunx expo run:android --variant=betaRelease --app-id=com.uniswap.mobile.beta", "options": { "cwd": "{projectRoot}" } }, "android:prod": { - "command": "rnef run:android --variant=prodDebug", + "command": "EXPO_ANDROID_LAUNCH_ACTIVITY='com.uniswap.mobile/com.uniswap.MainActivity' bunx expo run:android --variant=prodDebug", "options": { "cwd": "{projectRoot}" } }, "android:prod:release": { - "command": "rnef run:android --variant=prodRelease", + "command": "EXPO_ANDROID_LAUNCH_ACTIVITY='com.uniswap.mobile/com.uniswap.MainActivity' bunx expo run:android --variant=prodRelease", "options": { "cwd": "{projectRoot}" } @@ -196,7 +196,7 @@ } }, "ios": { - "command": "rnef run:ios --scheme Uniswap --configuration Debug && bun run start", + "command": "bunx expo run:ios --scheme Uniswap --configuration Debug", "options": { "cwd": "{projectRoot}" } @@ -208,31 +208,31 @@ } }, "ios:smol": { - "command": "rnef run:ios --device=\"iPhone SE (3rd generation)\"", + "command": "bunx expo run:ios --device=\"iPhone SE (3rd generation)\"", "options": { "cwd": "{projectRoot}" } }, "ios:dev:release": { - "command": "rnef run:ios --configuration Dev", + "command": "bunx expo run:ios --configuration Dev", "options": { "cwd": "{projectRoot}" } }, "ios:beta": { - "command": "rnef run:ios --configuration Beta", + "command": "bunx expo run:ios --configuration Beta", "options": { "cwd": "{projectRoot}" } }, "ios:bundle": { - "command": "rnef bundle --entry-file='index.js' --dev false --bundle-output='./ios/main.jsbundle' --sourcemap-output ./ios/main.jsbundle.map --dev=false --platform='ios' --assets-dest='./ios'", + "command": "bunx react-native bundle --entry-file apps/mobile/index.js --platform ios --dev false --bundle-output ./ios/main.jsbundle --assets-dest ./ios --sourcemap-output ./ios/main.jsbundle.map", "options": { "cwd": "{projectRoot}" } }, "ios:release": { - "command": "rnef run:ios --configuration Release", + "command": "bunx expo run:ios --configuration Release", "options": { "cwd": "{projectRoot}" } @@ -244,22 +244,25 @@ "lint:eslint": {}, "lint:eslint:fix": {}, "start": { - "command": "NODE_ENV=development rnef start --client-logs", + "command": "EXPO_ANDROID_LAUNCH_ACTIVITY='com.uniswap.mobile.dev/com.uniswap.MainActivity' EXPO_BUILD_CONFIGURATION=Debug NODE_ENV=development bunx expo start --scheme uniswap", "options": { "cwd": "{projectRoot}" - } + }, + "dependsOn": ["pod:ensure", "android:ensure"] }, "start:e2e": { - "command": "NODE_ENV=development IS_E2E_TEST=true rnef start --client-logs", + "command": "NODE_ENV=development IS_E2E_TEST=true bunx expo start", "options": { "cwd": "{projectRoot}" - } + }, + "dependsOn": ["pod:ensure", "android:ensure"] }, "start:production": { - "command": "NODE_ENV=production rnef start --reset-cache", + "command": "NODE_ENV=production bunx expo start --reset-cache", "options": { "cwd": "{projectRoot}" - } + }, + "dependsOn": ["pod:ensure", "android:ensure"] }, "test": { "command": "node --max-old-space-size=8912 ../../node_modules/.bin/jest", @@ -286,6 +289,27 @@ "cwd": "{projectRoot}" } }, + "pod:ensure": { + "command": "./scripts/check-podfile.sh", + "options": { + "cwd": "{projectRoot}" + }, + "inputs": ["{projectRoot}/ios/Podfile", "{projectRoot}/ios/Podfile.lock"], + "cache": true + }, + "android:ensure": { + "command": "./scripts/check-android-gradle.sh", + "options": { + "cwd": "{projectRoot}" + }, + "inputs": [ + "{projectRoot}/android/build.gradle", + "{projectRoot}/android/app/build.gradle", + "{projectRoot}/android/settings.gradle", + "{projectRoot}/android/gradle.properties" + ], + "cache": true + }, "pod:update": { "command": "./scripts/podinstall.sh -u", "options": { diff --git a/apps/mobile/rnef.config.mjs b/apps/mobile/rnef.config.mjs deleted file mode 100644 index b513fc0efc3..00000000000 --- a/apps/mobile/rnef.config.mjs +++ /dev/null @@ -1,36 +0,0 @@ -import { platformAndroid } from '@rnef/platform-android' -import { platformIOS } from '@rnef/platform-ios' -import { pluginMetro } from '@rnef/plugin-metro' -import { providerGitHub } from '@rnef/provider-github' -import { config } from 'dotenv' -config({ path: '../../.env.defaults.local' }) - -const isGitHubAction = process.env.GITHUB_ACTIONS === 'true' - -export default { - plugins: [pluginMetro()], - platforms: { - ios: platformIOS(), - android: platformAndroid(), - }, - remoteCacheProvider: isGitHubAction - ? 'github-actions' - : providerGitHub({ - owner: 'uniswap', - repository: 'universe', - token: process.env.GH_TOKEN_RN_CLI, - }), - fingerprint: { - ignorePaths: [ - // Files generated by [GraphQL] Apollo Generate Swift script phase in Xcode, making fingerprint unstable when installing pods vs not - 'ios/OneSignalNotificationServiceExtension/Env.swift', - 'ios/WidgetsCore/Env.swift', - 'ios/WidgetsCore/MobileSchema/MobileSchema.graphql.swift', - 'ios/WidgetsCore/MobileSchema/Fragments/**/*', - 'ios/WidgetsCore/MobileSchema/Operations/**/*', - 'ios/WidgetsCore/MobileSchema/Schema/**/*', - // There's a setup script in Podfile that changes the podspec in node_modules, making fingerprint unstable when installing pods vs not - '../../node_modules/react-native-permissions/RNPermissions.podspec', - ], - }, -} diff --git a/apps/mobile/scripts/check-android-gradle.sh b/apps/mobile/scripts/check-android-gradle.sh new file mode 100755 index 00000000000..775576a1897 --- /dev/null +++ b/apps/mobile/scripts/check-android-gradle.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +# This script warns users if they need to sync Gradle +# It's designed to be used with NX caching - NX will only run this +# when Android Gradle files change + +# Detect if we're in a workspace (monorepo) by checking for workspace root +# Script runs from apps/mobile, so check if ../../nx.json exists (workspace root) +if [ -f "../../nx.json" ]; then + # We're in a workspace, user likely runs commands from root + ANDROID_CMD="bun mobile android" +else + # We're not in a workspace, user runs commands from mobile dir + ANDROID_CMD="bun android" +fi + +echo "⚠️ Warning: Android Gradle files have changed since last build" +echo "" +echo "You may encounter issues when running the Android app." +echo "To fix this, run one of the following:" +echo " • $ANDROID_CMD (build Android app, which will sync Gradle automatically)" +echo "" +echo "Metro bundler will continue starting, but you should build the Android app before" +echo "attempting to run it to ensure Gradle dependencies are synced." + diff --git a/apps/mobile/scripts/check-podfile.sh b/apps/mobile/scripts/check-podfile.sh new file mode 100755 index 00000000000..4aec05ffce3 --- /dev/null +++ b/apps/mobile/scripts/check-podfile.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +# This script warns users if they need to run pod install +# It's designed to be used with NX caching - NX will only run this +# when Podfile or Podfile.lock changes + +# Detect if we're in a workspace (monorepo) by checking for workspace root +# Script runs from apps/mobile, so check if ../../nx.json exists (workspace root) +if [ -f "../../nx.json" ]; then + # We're in a workspace, user likely runs commands from root + IOS_CMD="bun mobile ios" +else + # We're not in a workspace, user runs commands from mobile dir + IOS_CMD="bun ios" +fi + +echo "⚠️ Warning: Podfile or Podfile.lock has changed since last pod install" +echo "" +echo "You may encounter issues when running the iOS app." +echo "To fix this, run:" +echo " • $IOS_CMD (build iOS app, which will install pods automatically)" +echo "" +echo "Metro bundler will continue starting, but you should build the iOS app before" +echo "attempting to run it to ensure pods are installed." + diff --git a/apps/mobile/scripts/checkBundleSize.sh b/apps/mobile/scripts/checkBundleSize.sh index b9a4e5a57da..e72af794fa6 100755 --- a/apps/mobile/scripts/checkBundleSize.sh +++ b/apps/mobile/scripts/checkBundleSize.sh @@ -1,5 +1,5 @@ #!/bin/bash -MAX_SIZE=24.60 +MAX_SIZE=24.50 MAX_BUFFER=0.5 # Check OS type and use appropriate stat command diff --git a/apps/mobile/scripts/getFingerprintForRadonIDE.ts b/apps/mobile/scripts/getFingerprintForRadonIDE.ts new file mode 100644 index 00000000000..61d1e332131 --- /dev/null +++ b/apps/mobile/scripts/getFingerprintForRadonIDE.ts @@ -0,0 +1,22 @@ +#!/usr/bin/env bun +import { createProjectHashAsync } from '@expo/fingerprint' + +async function main(): Promise { + try { + const projectRoot = process.cwd() + const hash = await createProjectHashAsync(projectRoot, { + silent: true, + }) + console.log(hash) + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + console.error(`Failed to generate fingerprint: ${errorMessage}`) + process.exit(1) + } +} + +main().catch((error) => { + const errorMessage = error instanceof Error ? error.message : String(error) + console.error(`Fatal error: ${errorMessage}`) + process.exit(1) +}) diff --git a/apps/mobile/scripts/ios-build-interactive/main.ts b/apps/mobile/scripts/ios-build-interactive/main.ts index db54dae187a..131f4c54749 100755 --- a/apps/mobile/scripts/ios-build-interactive/main.ts +++ b/apps/mobile/scripts/ios-build-interactive/main.ts @@ -219,11 +219,11 @@ const resetMetroCache = async (): Promise => { const buildForSimulator = async (config: BuildConfig): Promise => { printBuildInfo(config, 'iOS Simulator') - const args = ['rnef', 'run:ios', '--scheme', 'Uniswap', '--configuration', config.configuration] + const args = ['expo', 'run:ios', '--scheme', 'Uniswap', '--configuration', config.configuration] if (config.simulator) { const simulatorName = config.simulator.split('(')[0]?.trim() - args.push(`--device="${simulatorName}"`) + args.push(`--device=${simulatorName}`) } log.info(`Command: bun run ${args.join(' ')}\n`) @@ -241,13 +241,13 @@ const buildForDevice = async (config: BuildConfig): Promise => { printBuildInfo(config, 'iOS Device') const args = [ - 'rnef', + 'expo', 'run:ios', '--scheme', config.scheme, '--configuration', config.configuration, - '--destination', + '--device', 'device', ] diff --git a/apps/mobile/src/app/migrations.test.ts b/apps/mobile/src/app/migrations.test.ts index 55b7a04a610..76b64ac21ae 100644 --- a/apps/mobile/src/app/migrations.test.ts +++ b/apps/mobile/src/app/migrations.test.ts @@ -96,6 +96,8 @@ import { v90Schema, v91Schema, v92Schema, + v93Schema, + v95Schema, } from 'src/app/schema' import { persistConfig } from 'src/app/store' import { initialBiometricsSettingsState } from 'src/features/biometricsSettings/slice' @@ -105,6 +107,7 @@ import { initialPushNotificationsState } from 'src/features/notifications/slice' import { initialTweaksState } from 'src/features/tweaks/slice' import { initialWalletConnectState } from 'src/features/walletConnect/walletConnectSlice' import { ScannerModalState } from 'uniswap/src/components/ReceiveQRCode/constants' +import { USDC } from 'uniswap/src/constants/tokens' import { AccountType } from 'uniswap/src/features/accounts/types' import { initialUniswapBehaviorHistoryState } from 'uniswap/src/features/behaviorHistory/slice' import { UniverseChainId } from 'uniswap/src/features/chains/types' @@ -114,11 +117,16 @@ import { initialNotificationsState } from 'uniswap/src/features/notifications/sl import { initialSearchHistoryState } from 'uniswap/src/features/search/searchHistorySlice' import { initialUserSettingsState } from 'uniswap/src/features/settings/slice' import { ModalName } from 'uniswap/src/features/telemetry/constants' -import { initialTokensState } from 'uniswap/src/features/tokens/slice/slice' +import { initialTokensState } from 'uniswap/src/features/tokens/warnings/slice/slice' import { initialTransactionsState } from 'uniswap/src/features/transactions/slice' import { TransactionStatus, TransactionType } from 'uniswap/src/features/transactions/types/transactionDetails' import { initialVisibilityState } from 'uniswap/src/features/visibility/slice' -import { testMigrateSearchHistory, testRemoveTHBFromCurrency } from 'uniswap/src/state/uniswapMigrationTests' +import { + testAddActivityVisibility, + testMigrateDismissedTokenWarnings, + testMigrateSearchHistory, + testRemoveTHBFromCurrency, +} from 'uniswap/src/state/uniswapMigrationTests' import { transactionDetails } from 'uniswap/src/test/fixtures' import { DappRequestType } from 'uniswap/src/types/walletConnect' import { getAllKeysOfNestedObject } from 'utilities/src/primitives/objects' @@ -1784,4 +1792,24 @@ describe('Redux state migrations', () => { it('migrates from v92 to v93', () => { testMigrateSearchHistory(migrations[93], v92Schema) }) + + it('migrates from v93 to v95', () => { + testAddActivityVisibility(migrations[95], v93Schema) + }) + + it('migrates from v95 to v96', () => { + testMigrateDismissedTokenWarnings(migrations[96], { + ...v95Schema, + tokens: { + dismissedTokenWarnings: { + [UniverseChainId.Mainnet]: { + [USDC.address]: { + chainId: UniverseChainId.Mainnet, + address: USDC.address, + }, + }, + }, + }, + }) + }) }) diff --git a/apps/mobile/src/app/migrations.ts b/apps/mobile/src/app/migrations.ts index 779ea388446..8017c636abf 100644 --- a/apps/mobile/src/app/migrations.ts +++ b/apps/mobile/src/app/migrations.ts @@ -18,7 +18,9 @@ import { TransactionType, } from 'uniswap/src/features/transactions/types/transactionDetails' import { + addActivityVisibility, addDismissedBridgedAndCompatibleWarnings, + migrateDismissedTokenWarnings, migrateSearchHistory, removeThaiBahtFromFiatCurrency, unchecksumDismissedTokenWarningKeys, @@ -649,7 +651,8 @@ export const migrations = { const newNftKey = nftKey && tokenId && getNFTAssetKey(nftKey, tokenId) const accountNftsData = nftsData[accountAddress] - if (newNftKey && accountNftsData) { + + if (newNftKey) { accountNftsData[newNftKey] = { isHidden: true } } } @@ -1084,6 +1087,8 @@ export const migrations = { 93: migrateSearchHistory, 94: addDismissedBridgedAndCompatibleWarnings, + 95: addActivityVisibility, + 96: migrateDismissedTokenWarnings, } -export const MOBILE_STATE_VERSION = 94 +export const MOBILE_STATE_VERSION = 96 diff --git a/apps/mobile/src/app/modals/AccountSwitcherModal.tsx b/apps/mobile/src/app/modals/AccountSwitcherModal.tsx index de176e99439..aa6cb7b89f3 100644 --- a/apps/mobile/src/app/modals/AccountSwitcherModal.tsx +++ b/apps/mobile/src/app/modals/AccountSwitcherModal.tsx @@ -1,4 +1,5 @@ import { useIsFocused } from '@react-navigation/core' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import React, { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' @@ -12,9 +13,13 @@ import { Button, Flex, Text, TouchableArea, useSporeColors } from 'ui/src' import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions' import { spacing } from 'ui/src/theme' import { AddressDisplay } from 'uniswap/src/components/accounts/AddressDisplay' +import { buildWrappedUrl } from 'uniswap/src/components/banners/shared/utils' +import { UniswapWrapped2025Card } from 'uniswap/src/components/banners/UniswapWrapped2025Card/UniswapWrapped2025Card' import { ActionSheetModal, MenuItemProp } from 'uniswap/src/components/modals/ActionSheetModal' import { Modal } from 'uniswap/src/components/modals/Modal' -import { AccountType, DisplayNameType } from 'uniswap/src/features/accounts/types' +import { UNISWAP_WEB_URL } from 'uniswap/src/constants/urls' +import { AccountType } from 'uniswap/src/features/accounts/types' +import { setHasDismissedUniswapWrapped2025Banner } from 'uniswap/src/features/behaviorHistory/slice' import { Platform } from 'uniswap/src/features/platforms/types/Platform' import { ElementName, ModalName, WalletEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' @@ -23,13 +28,15 @@ import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding' import { MobileScreens, OnboardingScreens } from 'uniswap/src/types/screens/mobile' import { areAddressesEqual } from 'uniswap/src/utils/addresses' +import { openUri } from 'uniswap/src/utils/linking' +import { logger } from 'utilities/src/logger/logger' import { isAndroid } from 'utilities/src/platform' import { PlusCircle } from 'wallet/src/components/icons/PlusCircle' import { createOnboardingAccount } from 'wallet/src/features/onboarding/createOnboardingAccount' import { BackupType } from 'wallet/src/features/wallet/accounts/types' import { hasBackup } from 'wallet/src/features/wallet/accounts/utils' import { createAccountsActions } from 'wallet/src/features/wallet/create/createAccountsSaga' -import { useActiveAccountAddress, useDisplayName, useNativeAccountExists } from 'wallet/src/features/wallet/hooks' +import { useActiveAccountAddress, useNativeAccountExists } from 'wallet/src/features/wallet/hooks' import { selectAllAccountsSorted, selectSortedSignerMnemonicAccounts } from 'wallet/src/features/wallet/selectors' import { setAccountAsActive } from 'wallet/src/features/wallet/slice' @@ -59,9 +66,8 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme const hasImportedSeedPhrase = useNativeAccountExists() const isModalOpen = useIsFocused() const { openWalletRestoreModal, walletRestoreType } = useWalletRestore() - const displayName = useDisplayName(activeAccountAddress) - const activeAccountHasENS = displayName?.type === DisplayNameType.ENS + const isWrappedBannerEnabled = useFeatureFlag(FeatureFlags.UniswapWrapped2025) const sortedMnemonicAccounts = useSelector(selectSortedSignerMnemonicAccounts) @@ -99,6 +105,21 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme }) } + const onPressWrappedCard = useCallback(async () => { + if (!activeAccountAddress) { + return + } + + try { + const url = buildWrappedUrl(UNISWAP_WEB_URL, activeAccountAddress) + await openUri({ uri: url, openExternalBrowser: true }) + onClose() + dispatch(setHasDismissedUniswapWrapped2025Banner(true)) + } catch (error) { + logger.error(error, { tags: { file: 'AccountSwitcherModal', function: 'onPressWrappedCard' } }) + } + }, [activeAccountAddress, onClose, dispatch]) + const addWalletOptions = useMemo(() => { const createAdditionalAccount = async (): Promise => { // Generate new account @@ -270,19 +291,22 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme size={spacing.spacing60 - spacing.spacing4} variant="subheading1" /> - {!activeAccountHasENS && ( + {isWrappedBannerEnabled && ( - + )} + + + @@ -513,16 +564,9 @@ exports[`AccountSwitcher renders correctly 1`] = ` line-height-disabled="true" maxFontSizeMultiplier={1.2} numberOfLines={1} - onBlur={[Function]} - onFocus={[Function]} style={ { - "color": { - "dynamic": { - "dark": "#FFFFFF", - "light": "#131313", - }, - }, + "color": "#131313", "fontFamily": "Basel Grotesk", "fontSize": 17, "fontWeight": "500", @@ -586,11 +630,35 @@ exports[`AccountSwitcher renders correctly 1`] = ` collapsable={false} focusVisibleStyle={{}} forwardedRef={[Function]} + jestAnimatedProps={ + { + "value": {}, + } + } jestAnimatedStyle={ { "value": {}, } } + jestInlineStyle={ + [ + { + "backgroundColor": "transparent", + "borderBottomLeftRadius": 12, + "borderBottomRightRadius": 12, + "borderTopLeftRadius": 12, + "borderTopRightRadius": 12, + "flexDirection": "column", + "marginTop": 16, + "opacity": 1, + "transform": [ + { + "scale": 1, + }, + ], + }, + ] + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -603,27 +671,28 @@ exports[`AccountSwitcher renders correctly 1`] = ` onStartShouldSetResponder={[Function]} role="button" style={ - { - "backgroundColor": "transparent", - "borderBottomLeftRadius": 12, - "borderBottomRightRadius": 12, - "borderTopLeftRadius": 12, - "borderTopRightRadius": 12, - "flexDirection": "column", - "marginTop": 16, - "opacity": 1, - "transform": [ - { - "scale": 1, - }, - ], - } + [ + { + "backgroundColor": "transparent", + "borderBottomLeftRadius": 12, + "borderBottomRightRadius": 12, + "borderTopLeftRadius": 12, + "borderTopRightRadius": 12, + "flexDirection": "column", + "marginTop": 16, + "opacity": 1, + "transform": [ + { + "scale": 1, + }, + ], + }, + {}, + ] } testID="account-switcher-add-wallet" > = { reducer: executeSwapReducer, actions: executeSwapActions, }, - [swapSagaName]: { - name: swapSagaName, - wrappedSaga: swapSaga, - reducer: swapReducer, - actions: swapActions, - }, - [tokenWrapSagaName]: { - name: tokenWrapSagaName, - wrappedSaga: tokenWrapSaga, - reducer: tokenWrapReducer, - actions: tokenWrapActions, - }, [removeDelegationSagaName]: { name: removeDelegationSagaName, wrappedSaga: removeDelegationSaga, diff --git a/apps/mobile/src/app/navigation/NavigationContainer.tsx b/apps/mobile/src/app/navigation/NavigationContainer.tsx index 3300418d114..42ecef4f228 100644 --- a/apps/mobile/src/app/navigation/NavigationContainer.tsx +++ b/apps/mobile/src/app/navigation/NavigationContainer.tsx @@ -91,7 +91,7 @@ export const NavigationContainer: FC> = ({ children, on const useManageDeepLinks = (): void => { const dispatch = useDispatch() - const urlListener = useRef() + const urlListener = useRef(undefined) const deepLinkMutation = useMutation({ mutationFn: async () => { diff --git a/apps/mobile/src/app/navigation/constants.ts b/apps/mobile/src/app/navigation/constants.ts new file mode 100644 index 00000000000..778a6d63580 --- /dev/null +++ b/apps/mobile/src/app/navigation/constants.ts @@ -0,0 +1,2 @@ +// Some pages in react native navigation require a delay before the modal is opened +export const MODAL_OPEN_WAIT_TIME = 300 diff --git a/apps/mobile/src/app/navigation/navigation.tsx b/apps/mobile/src/app/navigation/navigation.tsx index 07cbfafd88c..889fbf8ea67 100644 --- a/apps/mobile/src/app/navigation/navigation.tsx +++ b/apps/mobile/src/app/navigation/navigation.tsx @@ -40,6 +40,8 @@ import { PasskeyHelpModalScreen } from 'src/components/modals/ReactNavigationMod import { PasskeyManagementModalScreen } from 'src/components/modals/ReactNavigationModals/PasskeyManagementModalScreen' import { PermissionsSettingsScreen } from 'src/components/modals/ReactNavigationModals/PermissionsSettingsScreen' import { PortfolioBalanceSettingsScreen } from 'src/components/modals/ReactNavigationModals/PortfolioBalanceSettingsScreen' +import { ReportTokenDataModalScreen } from 'src/components/modals/ReactNavigationModals/ReportTokenDataModalScreen' +import { ReportTokenIssueModalScreen } from 'src/components/modals/ReactNavigationModals/ReportTokenIssueModalScreen' import { SmartWalletEnabledModalScreen } from 'src/components/modals/ReactNavigationModals/SmartWalletEnabledModalScreen' import { SmartWalletNudgeScreen } from 'src/components/modals/ReactNavigationModals/SmartWalletNudgeScreen' import { TestnetModeModalScreen } from 'src/components/modals/ReactNavigationModals/TestnetModeModalScreen' @@ -419,6 +421,8 @@ export function AppStackNavigator(): JSX.Element { + + diff --git a/apps/mobile/src/app/navigation/tabs/CustomTabBar/CustomTabBar.tsx b/apps/mobile/src/app/navigation/tabs/CustomTabBar/CustomTabBar.tsx index 01de66c7fe1..d1b9cddd9bc 100644 --- a/apps/mobile/src/app/navigation/tabs/CustomTabBar/CustomTabBar.tsx +++ b/apps/mobile/src/app/navigation/tabs/CustomTabBar/CustomTabBar.tsx @@ -4,7 +4,7 @@ import type { LayoutChangeEvent } from 'react-native' import { useAnimatedStyle, useDerivedValue, withTiming } from 'react-native-reanimated' import { TAB_BAR_ANIMATION_DURATION, TAB_ITEMS } from 'src/app/navigation/tabs/CustomTabBar/constants' import { SwapButton } from 'src/app/navigation/tabs/SwapButton' -import { SwapLongPressModal } from 'src/app/navigation/tabs/SwapLongPressModal' +import { SwapLongPressOverlay } from 'src/app/navigation/tabs/SwapLongPressOverlay' import { Flex, TouchableArea, useIsDarkMode, useSporeColors } from 'ui/src' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { iconSizes, spacing } from 'ui/src/theme' @@ -165,7 +165,7 @@ export function CustomTabBar({ state, navigation }: BottomTabBarProps): JSX.Elem - (null) + const longPressTimerRef = useRef(null) const hasTriggeredLongPressHaptic = useRef(false) const activeAccountAddress = useActiveAccountAddressWithThrow() diff --git a/apps/mobile/src/app/navigation/tabs/SwapLongPressModal.tsx b/apps/mobile/src/app/navigation/tabs/SwapLongPressOverlay.tsx similarity index 54% rename from apps/mobile/src/app/navigation/tabs/SwapLongPressModal.tsx rename to apps/mobile/src/app/navigation/tabs/SwapLongPressOverlay.tsx index 234d81b234c..9b36790f87a 100644 --- a/apps/mobile/src/app/navigation/tabs/SwapLongPressModal.tsx +++ b/apps/mobile/src/app/navigation/tabs/SwapLongPressOverlay.tsx @@ -2,14 +2,15 @@ import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { BlurView } from 'expo-blur' import { type ReactNode, useMemo } from 'react' import { useTranslation } from 'react-i18next' -import { Modal, StyleSheet } from 'react-native' +import { StyleSheet, type ViewStyle } from 'react-native' +import Animated, { FadeIn, FadeOut } from 'react-native-reanimated' import { useDispatch } from 'react-redux' import { navigate } from 'src/app/navigation/rootNavigation' import { ESTIMATED_BOTTOM_TABS_HEIGHT } from 'src/app/navigation/tabs/CustomTabBar/constants' import { SwapButton } from 'src/app/navigation/tabs/SwapButton' import { useOpenReceiveModal } from 'src/features/modals/hooks/useOpenReceiveModal' import { openModal } from 'src/features/modals/modalSlice' -import { Flex, Text, TouchableArea, useSporeColors } from 'ui/src' +import { Flex, Text, TouchableArea, useIsDarkMode, useSporeColors } from 'ui/src' import { Bank, Buy, ReceiveAlt, SendAction } from 'ui/src/components/icons' import { iconSizes } from 'ui/src/theme' import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' @@ -19,19 +20,57 @@ import { useAppInsets } from 'uniswap/src/hooks/useAppInsets' import { isAndroid } from 'utilities/src/platform' import { useEvent } from 'utilities/src/react/hooks' +const ANIMATION_DURATION = 200 +const BASE_DELAY = 40 + +const AnimatedBlurView = Animated.createAnimatedComponent(BlurView) + +function AnimatedContainer({ + enteringDelay, + exitingDelay, + children, + style, + exitingDuration = ANIMATION_DURATION, +}: { + enteringDelay: number + exitingDelay: number + children: ReactNode + style?: ViewStyle + exitingDuration?: number +}): JSX.Element { + return ( + + {children} + + ) +} + const styles = StyleSheet.create({ blurView: { - flex: 1, + height: '100%', + left: 0, + position: 'absolute', + top: 0, + width: '100%', }, }) -interface SwapLongPressModalProps { +interface SwapLongPressOverlayProps { isVisible: boolean onClose: () => void onSwapLongPress: () => void } -export function SwapLongPressModal({ isVisible, onClose, onSwapLongPress }: SwapLongPressModalProps): JSX.Element { +export function SwapLongPressOverlay({ + isVisible, + onClose, + onSwapLongPress, +}: SwapLongPressOverlayProps): JSX.Element | null { const colors = useSporeColors() + const isDarkMode = useIsDarkMode() const insets = useAppInsets() const dispatch = useDispatch() const { t } = useTranslation() @@ -125,37 +164,72 @@ export function SwapLongPressModal({ isVisible, onClose, onSwapLongPress }: Swap [t, onReceivePress, onSendPress, onBuyPress, onSellPress, colors.accent1.val], ) + const NUM_OF_SWAP_MENU_ITEMS = swapMenuItems.length + const TOTAL_DELAY_FOR_EXIT_FROM_MENU_ITEMS = NUM_OF_SWAP_MENU_ITEMS * BASE_DELAY + + // Used for main container and SwapButton + const DELAY_FOR_MAIN_EXIT = TOTAL_DELAY_FOR_EXIT_FROM_MENU_ITEMS + ANIMATION_DURATION / 2 + + // We want to start animating the text out just before the main animation starts + const DELAY_FOR_SWAP_TEXT_EXIT = DELAY_FOR_MAIN_EXIT - ANIMATION_DURATION / 4 + + if (!isVisible) { + return null + } + return ( - - - - - {swapMenuItems.map((item) => ( - - ))} - - {/* Swap Button as last item in the column */} - + + + + {swapMenuItems.map((item, i) => { + const length = NUM_OF_SWAP_MENU_ITEMS + + // Wait for initial animation to complete, then animate each item in sequentially with a delay based on its position in the array. + const enteringDelay = ANIMATION_DURATION + (length - i) * BASE_DELAY + + // We want to start animating before the main animation starts + const exitingDelay = i * BASE_DELAY + + return ( + + + + ) + })} + + {/* Swap Button as last item in the column */} + + {/* We want to delay animating the text entering so the Swap button shows first, then the text animates in, followed by the actions + + Text animates out at the same time as the Swap button, so it looks like the text is animating in while the Swap button is animating out. + */} + {t('common.button.swap')} + + {/* Swap Button doesn't animate in so it feels like the same button that was long pressed on HomeScreen is still there. It is the final thing to animate out (along with the Text above). */} + - + - - - + + + ) } diff --git a/apps/mobile/src/app/navigation/types.ts b/apps/mobile/src/app/navigation/types.ts index c4d0cf03be1..c0ed40ff6cd 100644 --- a/apps/mobile/src/app/navigation/types.ts +++ b/apps/mobile/src/app/navigation/types.ts @@ -21,6 +21,8 @@ import { ReceiveCryptoModalState } from 'src/screens/ReceiveCryptoModalState' import { ViewPrivateKeysScreenState } from 'src/screens/ViewPrivateKeys/ViewPrivateKeysScreenState' import { BridgedAssetModalProps } from 'uniswap/src/components/BridgedAsset/BridgedAssetModal' import { WormholeModalProps } from 'uniswap/src/components/BridgedAsset/WormholeModal' +import { ReportTokenDataModalProps } from 'uniswap/src/components/reporting/ReportTokenDataModal' +import { ReportTokenModalProps } from 'uniswap/src/components/reporting/ReportTokenIssueModal' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { FORServiceProvider } from 'uniswap/src/features/fiatOnRamp/types' import { NFTItem } from 'uniswap/src/features/nfts/types' @@ -221,6 +223,8 @@ export type AppStackParamList = { [ModalName.ConfirmDisableSmartWalletScreen]: undefined [ModalName.BridgedAsset]: BridgedAssetModalProps [ModalName.Wormhole]: WormholeModalProps + [ModalName.ReportTokenIssue]: ReportTokenModalProps + [ModalName.ReportTokenData]: ReportTokenDataModalProps } export type AppStackNavigationProp = NativeStackNavigationProp diff --git a/apps/mobile/src/app/schema.ts b/apps/mobile/src/app/schema.ts index b396fd537ad..8f8a78eecc1 100644 --- a/apps/mobile/src/app/schema.ts +++ b/apps/mobile/src/app/schema.ts @@ -721,8 +721,18 @@ delete v92SchemaIntermediate.cloudBackup export const v92Schema = v92SchemaIntermediate -const v93Schema = v92Schema +export const v93Schema = v92Schema + +export const v95Schema = { + ...v93Schema, + visibility: { + ...v93Schema.visibility, + activity: {}, + }, +} + +const v96Schema = v95Schema // TODO: [MOB-201] use function with typed output when API reducers are removed from rootReducer // export const getSchema = (): RootState => v0Schema -export const getSchema = (): typeof v93Schema => v93Schema +export const getSchema = (): typeof v96Schema => v96Schema diff --git a/apps/mobile/src/components/PriceExplorer/usePriceHistory.test.ts b/apps/mobile/src/components/PriceExplorer/usePriceHistory.test.ts index 4d2669249db..1c1c44a6e83 100644 --- a/apps/mobile/src/components/PriceExplorer/usePriceHistory.test.ts +++ b/apps/mobile/src/components/PriceExplorer/usePriceHistory.test.ts @@ -3,6 +3,7 @@ import { GraphQLApi } from '@universe/api' import { act } from 'react-test-renderer' import { useTokenPriceHistory } from 'src/components/PriceExplorer/usePriceHistory' import { renderHookWithProviders } from 'src/test/render' +import { USDC, USDC_ARBITRUM, USDC_BASE, USDC_OPTIMISM, USDC_POLYGON } from 'uniswap/src/constants/tokens' import { getLatestPrice, priceHistory, @@ -34,6 +35,24 @@ const mockTokenProjectsQuery = (historyPrices: number[]) => (): GraphQLApi.Token const formatPriceHistory = (history: GraphQLApi.TimestampedAmount[]): Omit[] => history.map(({ timestamp, value }) => ({ value, timestamp: timestamp * 1000 })) +/** + * Creates a USDC token project with matching priceHistory for both the aggregated market + * and the Ethereum token's market. This ensures the hook returns the expected data since + * it prefers per-chain price history over aggregated price history. + */ +const createUsdcTokenProjectWithMatchingPriceHistory = ( + history: (GraphQLApi.TimestampedAmount | undefined)[], +): GraphQLApi.TokenProject => ({ + ...usdcTokenProject({ priceHistory: history }), + tokens: [ + token({ sdkToken: USDC, market: tokenMarket({ priceHistory: history }) }), + token({ sdkToken: USDC_POLYGON }), + token({ sdkToken: USDC_ARBITRUM }), + token({ sdkToken: USDC_BASE, market: tokenMarket() }), + token({ sdkToken: USDC_OPTIMISM }), + ], +}) + describe(useTokenPriceHistory, () => { it('returns correct initial values', async () => { const { result } = renderHookWithProviders(() => useTokenPriceHistory({ currencyId: SAMPLE_CURRENCY_ID_1 })) @@ -59,7 +78,13 @@ describe(useTokenPriceHistory, () => { it('returns on-chain spot price if off-chain spot price is not available', async () => { const market = tokenMarket() const { resolvers } = queryResolvers({ - tokenProjects: () => [usdcTokenProject({ markets: undefined, tokens: [token({ market })] })], + tokenProjects: () => [ + usdcTokenProject({ + markets: undefined, + // Ensure token has the correct chain to match SAMPLE_CURRENCY_ID_1 (Ethereum) + tokens: [token({ market, chain: GraphQLApi.Chain.Ethereum })], + }), + ], }) const { result } = renderHookWithProviders(() => useTokenPriceHistory({ currencyId: SAMPLE_CURRENCY_ID_1 }), { resolvers, @@ -80,6 +105,35 @@ describe(useTokenPriceHistory, () => { }) }) + it('handles gracefully when no token matches the currencyId chain', async () => { + const aggregatedMarket = tokenProjectMarket() + const { resolvers } = queryResolvers({ + tokenProjects: () => [ + usdcTokenProject({ + markets: [aggregatedMarket], + // Provide tokens for different chains, but none matching SAMPLE_CURRENCY_ID_1 (Ethereum) + tokens: [token({ chain: GraphQLApi.Chain.Polygon }), token({ chain: GraphQLApi.Chain.Arbitrum })], + }), + ], + }) + const { result } = renderHookWithProviders(() => useTokenPriceHistory({ currencyId: SAMPLE_CURRENCY_ID_1 }), { + resolvers, + }) + + await waitFor(() => { + expect(result.current.loading).toBe(false) + expect(result.current.error).toBe(false) + }) + + // Should fall back to aggregated market data when no chain-specific token is found + await waitFor(() => { + expect(result.current.data?.spot).toEqual({ + value: expect.objectContaining({ value: aggregatedMarket.price.value }), + relativeChange: expect.objectContaining({ value: aggregatedMarket.pricePercentChange24h.value }), + }) + }) + }) + describe('correct number of digits', () => { it('for max price greater than 1', async () => { const { resolvers } = queryResolvers({ @@ -141,7 +195,7 @@ describe(useTokenPriceHistory, () => { it('properly formats price history entries', async () => { const history = priceHistory() const { resolvers } = queryResolvers({ - tokenProjects: () => [usdcTokenProject({ priceHistory: history })], + tokenProjects: () => [createUsdcTokenProjectWithMatchingPriceHistory(history)], }) const { result } = renderHookWithProviders(() => useTokenPriceHistory({ currencyId: SAMPLE_CURRENCY_ID_1 }), { resolvers, @@ -156,12 +210,9 @@ describe(useTokenPriceHistory, () => { }) it('filters out invalid price history entries', async () => { + const invalidHistory = [undefined, timestampedAmount({ value: 1 }), undefined, timestampedAmount({ value: 2 })] const { resolvers } = queryResolvers({ - tokenProjects: () => [ - usdcTokenProject({ - priceHistory: [undefined, timestampedAmount({ value: 1 }), undefined, timestampedAmount({ value: 2 })], - }), - ], + tokenProjects: () => [createUsdcTokenProjectWithMatchingPriceHistory(invalidHistory)], }) const { result } = renderHookWithProviders(() => useTokenPriceHistory({ currencyId: SAMPLE_CURRENCY_ID_1 }), { resolvers, @@ -191,10 +242,10 @@ describe(useTokenPriceHistory, () => { const monthPriceHistory = priceHistory({ duration: GraphQLApi.HistoryDuration.Month }) const yearPriceHistory = priceHistory({ duration: GraphQLApi.HistoryDuration.Year }) - const dayTokenProject = usdcTokenProject({ priceHistory: dayPriceHistory }) - const weekTokenProject = usdcTokenProject({ priceHistory: weekPriceHistory }) - const monthTokenProject = usdcTokenProject({ priceHistory: monthPriceHistory }) - const yearTokenProject = usdcTokenProject({ priceHistory: yearPriceHistory }) + const dayTokenProject = createUsdcTokenProjectWithMatchingPriceHistory(dayPriceHistory) + const weekTokenProject = createUsdcTokenProjectWithMatchingPriceHistory(weekPriceHistory) + const monthTokenProject = createUsdcTokenProjectWithMatchingPriceHistory(monthPriceHistory) + const yearTokenProject = createUsdcTokenProjectWithMatchingPriceHistory(yearPriceHistory) const { resolvers } = queryResolvers({ // eslint-disable-next-line max-params @@ -239,10 +290,11 @@ describe(useTokenPriceHistory, () => { }) await waitFor(() => { + const ethereumToken = dayTokenProject.tokens.find((t) => t.chain === GraphQLApi.Chain.Ethereum) expect(result.current.data?.spot).toEqual({ - value: expect.objectContaining({ value: dayTokenProject.markets[0]?.price.value }), + value: expect.objectContaining({ value: ethereumToken?.market?.price?.value }), relativeChange: expect.objectContaining({ - value: dayTokenProject.markets[0]?.pricePercentChange24h.value, + value: dayTokenProject.markets?.[0]?.pricePercentChange24h?.value, }), }) }) @@ -273,7 +325,7 @@ describe(useTokenPriceHistory, () => { }) }) - it('returns correct spot price', async () => { + it('returns correct spot price with calculated percentage change', async () => { const { result } = renderHookWithProviders( () => useTokenPriceHistory({ @@ -283,10 +335,16 @@ describe(useTokenPriceHistory, () => { { resolvers }, ) await waitFor(() => { + const ethereumToken = yearTokenProject.tokens.find((t) => t.chain === GraphQLApi.Chain.Ethereum) + // For non-Day durations, relativeChange is calculated from price history + const openPrice = yearPriceHistory[0]?.value ?? 0 + const closePrice = yearPriceHistory[yearPriceHistory.length - 1]?.value ?? 0 + const calculatedChange = openPrice > 0 ? ((closePrice - openPrice) / openPrice) * 100 : 0 + expect(result.current.data?.spot).toEqual({ - value: expect.objectContaining({ value: yearTokenProject.markets[0]?.price?.value }), + value: expect.objectContaining({ value: ethereumToken?.market?.price?.value }), relativeChange: expect.objectContaining({ - value: yearTokenProject.markets[0]?.pricePercentChange24h?.value, + value: calculatedChange, }), }) }) @@ -294,18 +352,20 @@ describe(useTokenPriceHistory, () => { }) describe('when duration is changed', () => { - it('returns new price history and spot price', async () => { + it('returns new price history and spot price with correct percentage change calculation', async () => { const { result } = renderHookWithProviders(() => useTokenPriceHistory({ currencyId: SAMPLE_CURRENCY_ID_1 }), { resolvers, }) await waitFor(() => { + const ethereumToken = dayTokenProject.tokens.find((t) => t.chain === GraphQLApi.Chain.Ethereum) + // For Day duration, should use API's 24hr value expect(result.current.data).toEqual({ priceHistory: formatPriceHistory(dayPriceHistory), spot: { - value: expect.objectContaining({ value: dayTokenProject.markets[0]?.price.value }), + value: expect.objectContaining({ value: ethereumToken?.market?.price?.value }), relativeChange: expect.objectContaining({ - value: dayTokenProject.markets[0]?.pricePercentChange24h.value, + value: dayTokenProject.markets?.[0]?.pricePercentChange24h?.value, }), }, }) @@ -317,12 +377,18 @@ describe(useTokenPriceHistory, () => { }) await waitFor(() => { + const ethereumToken = weekTokenProject.tokens.find((t) => t.chain === GraphQLApi.Chain.Ethereum) + // For Week duration, should calculate from price history + const openPrice = weekPriceHistory[0]?.value ?? 0 + const closePrice = weekPriceHistory[weekPriceHistory.length - 1]?.value ?? 0 + const calculatedChange = openPrice > 0 ? ((closePrice - openPrice) / openPrice) * 100 : 0 + expect(result.current.data).toEqual({ priceHistory: formatPriceHistory(weekPriceHistory), spot: { - value: expect.objectContaining({ value: weekTokenProject.markets[0]?.price?.value }), + value: expect.objectContaining({ value: ethereumToken?.market?.price?.value }), relativeChange: expect.objectContaining({ - value: weekTokenProject.markets[0]?.pricePercentChange24h?.value, + value: calculatedChange, }), }, }) diff --git a/apps/mobile/src/components/Requests/ConnectedDapps/DappConnectionItem.tsx b/apps/mobile/src/components/Requests/ConnectedDapps/DappConnectionItem.tsx index 2f7d54b1a79..f854756545e 100644 --- a/apps/mobile/src/components/Requests/ConnectedDapps/DappConnectionItem.tsx +++ b/apps/mobile/src/components/Requests/ConnectedDapps/DappConnectionItem.tsx @@ -4,11 +4,11 @@ import { NativeSyntheticEvent, StyleSheet } from 'react-native' import ContextMenu, { ContextMenuOnPressNativeEvent } from 'react-native-context-menu-view' import 'react-native-reanimated' import { FadeIn, FadeOut } from 'react-native-reanimated' -import { DappHeaderIcon } from 'src/components/Requests/DappHeaderIcon' import { WalletConnectSession } from 'src/features/walletConnect/walletConnectSlice' import { AnimatedTouchableArea, Flex, Text } from 'ui/src' import { iconSizes, spacing } from 'ui/src/theme' import { noop } from 'utilities/src/react/noop' +import { DappHeaderIcon } from 'wallet/src/components/dappRequests/DappHeaderIcon' export function DappConnectionItem({ session, @@ -74,7 +74,7 @@ export function DappConnectionItem({ )} - + {dappRequestInfo.name || dappRequestInfo.url} diff --git a/apps/mobile/src/components/Requests/ModalWithOverlay/ModalWithOverlay.tsx b/apps/mobile/src/components/Requests/ModalWithOverlay/ModalWithOverlay.tsx index 4ea51ca8353..39392c5ebd2 100644 --- a/apps/mobile/src/components/Requests/ModalWithOverlay/ModalWithOverlay.tsx +++ b/apps/mobile/src/components/Requests/ModalWithOverlay/ModalWithOverlay.tsx @@ -58,7 +58,7 @@ export function ModalWithOverlay({ }: ModalWithOverlayProps): JSX.Element { const scrollViewRef = useRef(null) const contentViewRef = useRef(null) - const measureLayoutTimeoutRef = useRef() + const measureLayoutTimeoutRef = useRef(undefined) const startedScrollingRef = useRef(false) const [showOverlay, setShowOverlay] = useState(false) @@ -126,7 +126,6 @@ export function ModalWithOverlay({ contentContainerStyle={ contentContainerStyle ?? { paddingHorizontal: spacing.spacing24, - paddingTop: spacing.spacing12, } } showsVerticalScrollIndicator={false} diff --git a/apps/mobile/src/components/Requests/RequestModal/ClientDetails.tsx b/apps/mobile/src/components/Requests/RequestModal/ClientDetails.tsx index 69120a67ad8..2fb263be61e 100644 --- a/apps/mobile/src/components/Requests/RequestModal/ClientDetails.tsx +++ b/apps/mobile/src/components/Requests/RequestModal/ClientDetails.tsx @@ -1,12 +1,9 @@ import React from 'react' -import { DappHeaderIcon } from 'src/components/Requests/DappHeaderIcon' import { HeaderText } from 'src/components/Requests/RequestModal/HeaderText' import { WalletConnectSigningRequest } from 'src/features/walletConnect/walletConnectSlice' -import { Flex, useSporeColors } from 'ui/src' -import { iconSizes } from 'ui/src/theme' import { useCurrencyInfo } from 'uniswap/src/features/tokens/useCurrencyInfo' -import { formatDappURL } from 'utilities/src/format/urls' -import { LinkButton } from 'wallet/src/components/buttons/LinkButton' +import { DappHeaderIcon } from 'wallet/src/components/dappRequests/DappHeaderIcon' +import { DappRequestHeader } from 'wallet/src/components/dappRequests/DappRequestHeader' export interface PermitInfo { currencyId: string @@ -21,26 +18,11 @@ export function ClientDetails({ permitInfo?: PermitInfo }): JSX.Element { const { dappRequestInfo } = request - const colors = useSporeColors() - const permitCurrencyInfo = useCurrencyInfo(permitInfo?.currencyId) - - return ( - - - - - + const headerIcon = + const title = ( + ) + + return } diff --git a/apps/mobile/src/components/Requests/RequestModal/HeaderText.tsx b/apps/mobile/src/components/Requests/RequestModal/HeaderText.tsx index 3a03d374293..5fe9b474ce5 100644 --- a/apps/mobile/src/components/Requests/RequestModal/HeaderText.tsx +++ b/apps/mobile/src/components/Requests/RequestModal/HeaderText.tsx @@ -26,7 +26,7 @@ export function HeaderText({ })?.toExact() return readablePermitAmount ? ( - + }} @@ -39,7 +39,7 @@ export function HeaderText({ /> ) : ( - + }} @@ -70,9 +70,5 @@ export function HeaderText({ return } - return ( - - {getReadableMethodName(method, dappRequestInfo.name || dappRequestInfo.url)} - - ) + return {getReadableMethodName(method, dappRequestInfo.name || dappRequestInfo.url)} } diff --git a/apps/mobile/src/components/Requests/RequestModal/RequestDetails.tsx b/apps/mobile/src/components/Requests/RequestModal/RequestDetails.tsx index de2feb1b953..46767665dce 100644 --- a/apps/mobile/src/components/Requests/RequestModal/RequestDetails.tsx +++ b/apps/mobile/src/components/Requests/RequestModal/RequestDetails.tsx @@ -6,6 +6,7 @@ import { ScrollView } from 'react-native-gesture-handler' import { PermitInfo } from 'src/components/Requests/RequestModal/ClientDetails' import { isBatchedTransactionRequest, + isPersonalSignRequest, isTransactionRequest, SignRequest, WalletConnectSigningRequest, @@ -44,7 +45,7 @@ const requestMessageStyle: StyleProp = { } const getStrMessage = (request: WalletConnectSigningRequest): string => { - if (request.type === EthMethod.PersonalSign || request.type === EthMethod.EthSign) { + if (isPersonalSignRequest(request)) { return request.message || request.rawMessage } diff --git a/apps/mobile/src/components/Requests/RequestModal/WalletConnectRequestModal.tsx b/apps/mobile/src/components/Requests/RequestModal/WalletConnectRequestModal.tsx index 768181e6026..a4e7f666d49 100644 --- a/apps/mobile/src/components/Requests/RequestModal/WalletConnectRequestModal.tsx +++ b/apps/mobile/src/components/Requests/RequestModal/WalletConnectRequestModal.tsx @@ -2,7 +2,7 @@ import { useNetInfo } from '@react-native-community/netinfo' import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { getSdkError } from '@walletconnect/utils' import { providers } from 'ethers' -import React, { useMemo, useRef } from 'react' +import React, { useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' import { ModalWithOverlay } from 'src/components/Requests/ModalWithOverlay/ModalWithOverlay' @@ -28,18 +28,20 @@ import { } from 'src/features/walletConnect/walletConnectSlice' import { spacing } from 'ui/src/theme' import { EthMethod } from 'uniswap/src/features/dappRequests/types' -import { isSignTypedDataRequest } from 'uniswap/src/features/dappRequests/utils' +import { isSelfCallWithData, isSignTypedDataRequest } from 'uniswap/src/features/dappRequests/utils' import { useTransactionGasFee } from 'uniswap/src/features/gas/hooks' import { Platform } from 'uniswap/src/features/platforms/types/Platform' import { useHasAccountMismatchCallback } from 'uniswap/src/features/smartWallet/mismatch/hooks' import { MobileEventName, ModalName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' -import { useIsBlocked } from 'uniswap/src/features/trm/hooks' import { DappRequestType, UwULinkMethod, WCEventType, WCRequestOutcome } from 'uniswap/src/types/walletConnect' import { areAddressesEqual } from 'uniswap/src/utils/addresses' +import { useBooleanState } from 'utilities/src/react/useBooleanState' +import { TransactionRiskLevel } from 'wallet/src/features/dappRequests/types' +import { shouldDisableConfirm } from 'wallet/src/features/dappRequests/utils/riskUtils' import { formatExternalTxnWithGasEstimates } from 'wallet/src/features/gas/formatExternalTxnWithGasEstimates' -import { useIsBlockedActiveAddress } from 'wallet/src/features/trm/hooks' -import { useSignerAccounts } from 'wallet/src/features/wallet/hooks' +import { useLiveAccountDelegationDetails } from 'wallet/src/features/smartWallet/hooks/useLiveAccountDelegationDetails' +import { useHasSmartWalletConsent, useSignerAccounts } from 'wallet/src/features/wallet/hooks' interface Props { onClose: () => void @@ -61,8 +63,14 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem const netInfo = useNetInfo() const didOpenFromDeepLink = useSelector(selectDidOpenFromDeepLink) const chainId = request.chainId + // Initialize with null to indicate scan hasn't completed yet + const [riskLevel, setRiskLevel] = useState(null) + const { value: confirmedRisk, setValue: setConfirmedRisk } = useBooleanState(false) const enablePermitMismatchUx = useFeatureFlag(FeatureFlags.EnablePermitMismatchUX) + const enableEip5792Methods = useFeatureFlag(FeatureFlags.Eip5792Methods) + const hasSmartWalletConsent = useHasSmartWalletConsent() + const blockaidTransactionScanning = useFeatureFlag(FeatureFlags.BlockaidTransactionScanning) const tx: providers.TransactionRequest | undefined = useMemo(() => { if (isTransactionRequest(request)) { @@ -82,7 +90,33 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem addressInput2: { address: request.account, platform: Platform.EVM }, }), ) - const gasFee = useTransactionGasFee({ tx }) + const delegationData = useLiveAccountDelegationDetails({ + address: request.account, + chainId, + }) + // Check if this is a self-transaction (to === from) with data + // This is required for delegation to occur + // Note: chainId is required for correct address comparison + const isSelfTransaction = useMemo( + () => + isSelfCallWithData({ + from: request.account, + to: tx?.to, + data: tx?.data ? String(tx.data) : undefined, + chainId, + }), + [request.account, tx?.to, tx?.data, chainId], + ) + const shouldDelegate = Boolean( + delegationData?.needsDelegation && enableEip5792Methods && hasSmartWalletConsent && isSelfTransaction, + ) + const smartContractDelegationAddress = shouldDelegate + ? delegationData?.contractAddress // latest Uniswap delegation address + : delegationData?.currentDelegationAddress + const gasFee = useTransactionGasFee({ + tx, + ...(smartContractDelegationAddress && { smartContractDelegationAddress }), + }) const hasSufficientFunds = useHasSufficientFunds({ account: request.account, @@ -91,12 +125,6 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem value: tx?.value?.toString(), }) - const { isBlocked: isSenderBlocked, isBlockedLoading: isSenderBlockedLoading } = useIsBlockedActiveAddress() - const { isBlocked: isRecipientBlocked, isBlockedLoading: isRecipientBlockedLoading } = useIsBlocked(tx?.to) - - const isBlocked = isSenderBlocked || isRecipientBlocked - const isBlockedLoading = isSenderBlockedLoading || isRecipientBlockedLoading - const getHasMismatch = useHasAccountMismatchCallback() const hasMismatch = getHasMismatch(chainId) // When link mode is active we can sign messages through universal links on device @@ -111,8 +139,11 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem return false } - if (isBlocked || isBlockedLoading) { - return false + // If Blockaid scanning is enabled, disable confirm based on risk level and confirmation state + if (blockaidTransactionScanning) { + if (shouldDisableConfirm({ riskLevel, confirmedRisk })) { + return false + } } if (getDoesMethodCostGas(request)) { @@ -281,7 +312,7 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem ) diff --git a/apps/mobile/src/components/Requests/RequestModal/WalletConnectRequestModalContent.tsx b/apps/mobile/src/components/Requests/RequestModal/WalletConnectRequestModalContent.tsx index 86c391031a3..593d0dc401a 100644 --- a/apps/mobile/src/components/Requests/RequestModal/WalletConnectRequestModalContent.tsx +++ b/apps/mobile/src/components/Requests/RequestModal/WalletConnectRequestModalContent.tsx @@ -1,5 +1,6 @@ import { useBottomSheetInternal } from '@gorhom/bottom-sheet' import { useNetInfo } from '@react-native-community/netinfo' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { useTranslation } from 'react-i18next' import Animated, { useAnimatedStyle } from 'react-native-reanimated' import { ClientDetails, PermitInfo } from 'src/components/Requests/RequestModal/ClientDetails' @@ -12,15 +13,19 @@ import { import { Flex, Text } from 'ui/src' import { AlertTriangleFilled } from 'ui/src/components/icons' import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard' -import { nativeOnChain } from 'uniswap/src/constants/tokens' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' import { EthMethod } from 'uniswap/src/features/dappRequests/types' import { GasFeeResult } from 'uniswap/src/features/gas/types' -import { BlockedAddressWarning } from 'uniswap/src/features/transactions/modals/BlockedAddressWarning' -import { isPrimaryTypePermit } from 'uniswap/src/types/walletConnect' +import { isPrimaryTypePermit, UwULinkMethod } from 'uniswap/src/types/walletConnect' import { buildCurrencyId } from 'uniswap/src/utils/currencyId' import { logger } from 'utilities/src/logger/logger' import { MAX_HIDDEN_CALLS_BY_DEFAULT } from 'wallet/src/components/BatchedTransactions/BatchedTransactionDetails' +import { DappPersonalSignContent } from 'wallet/src/components/dappRequests/DappPersonalSignContent' +import { DappSendCallsScanningContent } from 'wallet/src/components/dappRequests/DappSendCallsScanningContent' +import { DappSignTypedDataContent } from 'wallet/src/components/dappRequests/DappSignTypedDataContent' +import { DappTransactionScanningContent } from 'wallet/src/components/dappRequests/DappTransactionScanningContent' import { WarningBox } from 'wallet/src/components/WarningBox/WarningBox' +import { TransactionRiskLevel } from 'wallet/src/features/dappRequests/types' import { AddressFooter } from 'wallet/src/features/transactions/TransactionRequest/AddressFooter' import { NetworkFeeFooter } from 'wallet/src/features/transactions/TransactionRequest/NetworkFeeFooter' @@ -58,21 +63,27 @@ type WalletConnectRequestModalContentProps = { gasFee: GasFeeResult hasSufficientFunds: boolean request: WalletConnectSigningRequest - isBlocked: boolean + showSmartWalletActivation?: boolean + confirmedRisk: boolean + onConfirmRisk: (confirmed: boolean) => void + onRiskLevelChange: (riskLevel: TransactionRiskLevel) => void } export function WalletConnectRequestModalContent({ request, hasSufficientFunds, - isBlocked, gasFee, + showSmartWalletActivation, + confirmedRisk, + onConfirmRisk, + onRiskLevelChange, }: WalletConnectRequestModalContentProps): JSX.Element { const chainId = request.chainId const permitInfo = getPermitInfo(request) - const nativeCurrency = nativeOnChain(chainId) + const nativeCurrency = getChainInfo(chainId).nativeCurrency - const { t } = useTranslation() const { animatedFooterHeight } = useBottomSheetInternal() + const blockaidTransactionScanning = useFeatureFlag(FeatureFlags.BlockaidTransactionScanning) const netInfo = useNetInfo() @@ -87,56 +98,111 @@ export function WalletConnectRequestModalContent({ return ( <> - + - - - - - - - {!hasSufficientFunds && ( - - - {t('walletConnect.request.error.insufficientFunds', { - currencySymbol: nativeCurrency.symbol ?? '', - })} - + {/* Show Blockaid scanning UI for supported request types */} + {blockaidTransactionScanning ? ( + <> + + + + - )} - - {!netInfo.isInternetReachable && !suppressOfflineWarning ? ( - } - textColor="$statusWarning" - title={t('walletConnect.request.error.network')} - /> - ) : ( - - )} - - + + + ) : ( + <> + {/* Fallback to original UI for non-scanning requests */} + + + + + + + + + + + + )} + + ) +} + +function RequestWarnings({ + request, + hasSufficientFunds, + isNetworkReachable, + suppressOfflineWarning, + nativeCurrencySymbol, +}: { + request: WalletConnectSigningRequest + hasSufficientFunds: boolean + isNetworkReachable: boolean + suppressOfflineWarning: boolean + nativeCurrencySymbol: string +}): JSX.Element { + const { t } = useTranslation() + + return ( + <> + {!hasSufficientFunds && ( + + + {t('walletConnect.request.error.insufficientFunds', { + currencySymbol: nativeCurrencySymbol, + })} + + + )} + + {!isNetworkReachable && !suppressOfflineWarning ? ( + } + textColor="$statusWarning" + title={t('walletConnect.request.error.network')} + /> + ) : ( + + )} ) } @@ -144,22 +210,16 @@ export function WalletConnectRequestModalContent({ function WarningSection({ request, showUnsafeWarning, - isBlockedAddress, }: { request: WalletConnectSigningRequest showUnsafeWarning: boolean - isBlockedAddress: boolean }): JSX.Element | null { const { t } = useTranslation() - if (!showUnsafeWarning && !isBlockedAddress) { + if (!showUnsafeWarning) { return null } - if (isBlockedAddress) { - return - } - if (isBatchedTransactionRequest(request)) { if (request.calls.length <= 1) { return null @@ -175,3 +235,97 @@ function WarningSection({ return null } + +/** Helper component to render appropriate scanning content based on request type */ +function ScanningContent({ + request, + chainId, + gasFee, + showSmartWalletActivation, + confirmedRisk, + onConfirmRisk, + onRiskLevelChange, +}: { + request: WalletConnectSigningRequest + chainId: number + gasFee: GasFeeResult + showSmartWalletActivation?: boolean + confirmedRisk: boolean + onConfirmRisk: (confirmed: boolean) => void + onRiskLevelChange: (riskLevel: TransactionRiskLevel) => void +}): JSX.Element { + switch (request.type) { + case EthMethod.EthSendTransaction: + case UwULinkMethod.Erc20Send: + return ( + + ) + + case EthMethod.PersonalSign: + case EthMethod.EthSign: + return ( + + ) + + case EthMethod.WalletSendCalls: + return ( + + ) + + case EthMethod.SignTypedData: + case EthMethod.SignTypedDataV4: + return ( + + ) + } +} diff --git a/apps/mobile/src/components/Requests/ScanSheet/PendingConnectionModal.tsx b/apps/mobile/src/components/Requests/ScanSheet/PendingConnectionModal.tsx index ba8b4b13594..6f64a0317be 100644 --- a/apps/mobile/src/components/Requests/ScanSheet/PendingConnectionModal.tsx +++ b/apps/mobile/src/components/Requests/ScanSheet/PendingConnectionModal.tsx @@ -1,15 +1,13 @@ import { useBottomSheetInternal } from '@gorhom/bottom-sheet' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { getSdkError } from '@walletconnect/utils' import React, { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import Animated, { useAnimatedStyle } from 'react-native-reanimated' import { useDispatch, useSelector } from 'react-redux' -import { DappHeaderIcon } from 'src/components/Requests/DappHeaderIcon' import { ModalWithOverlay, ModalWithOverlayProps } from 'src/components/Requests/ModalWithOverlay/ModalWithOverlay' -import { AccountSelectPopover } from 'src/components/Requests/ScanSheet/AccountSelectPopover' -import { SitePermissions } from 'src/components/Requests/ScanSheet/SitePermissions' import { selectDidOpenFromDeepLink } from 'src/features/walletConnect/selectors' -import { getSessionNamespaces } from 'src/features/walletConnect/utils' +import { convertCapabilitiesToScopedProperties, getSessionNamespaces } from 'src/features/walletConnect/utils' import { returnToPreviousApp } from 'src/features/walletConnect/WalletConnect' import { wcWeb3Wallet } from 'src/features/walletConnect/walletConnectClient' import { @@ -17,21 +15,28 @@ import { removePendingSession, setDidOpenFromDeepLink, WalletConnectPendingSession, - WalletConnectVerifyStatus, } from 'src/features/walletConnect/walletConnectSlice' -import { Flex, Text, useSporeColors } from 'ui/src' -import { Verified } from 'ui/src/components/icons' +import { Flex } from 'ui/src' import { AccountType } from 'uniswap/src/features/accounts/types' import { pushNotification } from 'uniswap/src/features/notifications/slice/slice' import { AppNotificationType } from 'uniswap/src/features/notifications/slice/types' import { MobileEventName, ModalName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { DappRequestType, WalletConnectEvent, WCEventType, WCRequestOutcome } from 'uniswap/src/types/walletConnect' -import { formatDappURL } from 'utilities/src/format/urls' import { useEvent } from 'utilities/src/react/hooks' import { ONE_SECOND_MS } from 'utilities/src/time/time' -import { LinkButton } from 'wallet/src/components/buttons/LinkButton' -import { useActiveAccountWithThrow, useSignerAccounts } from 'wallet/src/features/wallet/hooks' +import { DappConnectionContent } from 'wallet/src/components/dappRequests/DappConnectionContent' +import { DappRequestHeader } from 'wallet/src/components/dappRequests/DappRequestHeader' +import { getCapabilitiesCore } from 'wallet/src/features/batchedTransactions/utils' +import { useBlockaidVerification } from 'wallet/src/features/dappRequests/hooks/useBlockaidVerification' +import { useDappConnectionConfirmation } from 'wallet/src/features/dappRequests/hooks/useDappConnectionConfirmation' +import { DappConnectionInfo, DappVerificationStatus } from 'wallet/src/features/dappRequests/types' +import { mergeVerificationStatuses } from 'wallet/src/features/dappRequests/verification' +import { + useActiveAccountWithThrow, + useHasSmartWalletConsent, + useSignerAccounts, +} from 'wallet/src/features/wallet/hooks' type Props = { pendingSession: WalletConnectPendingSession @@ -47,12 +52,20 @@ export const PendingConnectionModal = ({ pendingSession, onClose }: Props): JSX. const isViewOnly = activeAccount.type === AccountType.Readonly const didOpenFromDeepLink = useSelector(selectDidOpenFromDeepLink) + const hasSmartWalletConsent = useHasSmartWalletConsent() + const eip5792MethodsEnabled = useFeatureFlag(FeatureFlags.Eip5792Methods) - const [confirmedWarning, setConfirmedWarning] = useState(false) const [isConnecting, setIsConnecting] = useState(false) - const isThreat = pendingSession.verifyStatus === WalletConnectVerifyStatus.Threat - const disableConfirm = (isThreat && !confirmedWarning) || isViewOnly || isConnecting + // Merge WalletConnect verification with Blockaid verification + const { verificationStatus: blockaidStatus } = useBlockaidVerification(pendingSession.dappRequestInfo.url) + const finalVerificationStatus = mergeVerificationStatuses(pendingSession.verifyStatus, blockaidStatus) + + const { confirmedWarning, setConfirmedWarning, disableConfirm } = useDappConnectionConfirmation({ + verificationStatus: finalVerificationStatus, + isViewOnly, + isLoading: isConnecting, + }) const signerAccounts = useSignerAccounts() const defaultSelectedAccountAddresses = useMemo(() => { @@ -103,10 +116,18 @@ export const PendingConnectionModal = ({ pendingSession, onClose }: Props): JSX. // Handle WC 2.0 session request if (approved) { const namespaces = getSessionNamespaces(orderedSelectedAccountAddresses, pendingSession.proposalNamespaces) + const capabilities = await getCapabilitiesCore({ + address: activeAddress, + chainIds: pendingSession.chains, + hasSmartWalletConsent: hasSmartWalletConsent ?? false, + }) + + const scopedProperties = convertCapabilitiesToScopedProperties(capabilities) const session = await wcWeb3Wallet.approveSession({ id: Number(pendingSession.id), namespaces, + ...(eip5792MethodsEnabled ? { scopedProperties } : {}), }) dispatch( @@ -122,6 +143,7 @@ export const PendingConnectionModal = ({ pendingSession, onClose }: Props): JSX. chains: pendingSession.chains, namespaces, activeAccount: activeAddress, + ...(eip5792MethodsEnabled ? { capabilities } : {}), }, }), ) @@ -156,8 +178,7 @@ export const PendingConnectionModal = ({ pendingSession, onClose }: Props): JSX. } }) - const dappName = pendingSession.dappRequestInfo.name || pendingSession.dappRequestInfo.url || '' - + const isThreat = finalVerificationStatus === DappVerificationStatus.Threat const isThreatProps: Partial = isThreat ? { cancelButtonText: t('walletConnect.pending.button.reject'), @@ -188,9 +209,8 @@ export const PendingConnectionModal = ({ pendingSession, onClose }: Props): JSX. > void - dappName: string pendingSession: WalletConnectPendingSession - verifyStatus: WalletConnectVerifyStatus + verifyStatus: DappVerificationStatus isViewOnly: boolean onConfirmWarning: (confirmed: boolean) => void confirmedWarning: boolean @@ -218,7 +237,6 @@ function PendingConnectionModalContent({ allAccountAddresses, selectedAccountAddresses, setSelectedAccountAddresses, - dappName, pendingSession, verifyStatus, isViewOnly, @@ -226,73 +244,37 @@ function PendingConnectionModalContent({ confirmedWarning, }: PendingConnectionModalContentProps): JSX.Element { const { t } = useTranslation() - const colors = useSporeColors() - const { animatedFooterHeight } = useBottomSheetInternal() const bottomSpacerStyle = useAnimatedStyle(() => ({ height: animatedFooterHeight.value, })) + const dappInfo: DappConnectionInfo = { + name: pendingSession.dappRequestInfo.name, + url: pendingSession.dappRequestInfo.url, + icon: pendingSession.dappRequestInfo.icon, + } + return ( <> - - - - {t('walletConnect.pending.title', { - dappName, - })} - - - - {verifyStatus === WalletConnectVerifyStatus.Verified && ( - - )} - + + - } onConfirmWarning={onConfirmWarning} /> - {!isViewOnly && ( - - - - )} - {isViewOnly && ( - - - {t('home.warning.viewOnly')} - - - )} - ) } diff --git a/apps/mobile/src/components/Requests/Uwulink/utils.ts b/apps/mobile/src/components/Requests/Uwulink/utils.ts index 5fede7fe5ea..f1d138acde1 100644 --- a/apps/mobile/src/components/Requests/Uwulink/utils.ts +++ b/apps/mobile/src/components/Requests/Uwulink/utils.ts @@ -147,6 +147,7 @@ export async function getFormattedUwuLinkTxnRequest({ dappRequestInfo: { name: '', url: '', + icon: null, ...request.dapp, requestType: DappRequestType.UwULink, chain_id: request.chainId, diff --git a/apps/mobile/src/components/RestoreWalletModal/__snapshots__/PrivateKeySpeedBumpModal.test.tsx.snap b/apps/mobile/src/components/RestoreWalletModal/__snapshots__/PrivateKeySpeedBumpModal.test.tsx.snap index 031dd009b31..a7a02717069 100644 --- a/apps/mobile/src/components/RestoreWalletModal/__snapshots__/PrivateKeySpeedBumpModal.test.tsx.snap +++ b/apps/mobile/src/components/RestoreWalletModal/__snapshots__/PrivateKeySpeedBumpModal.test.tsx.snap @@ -3,15 +3,7 @@ exports[`PrivateKeySpeedBumpModal renders correctly 1`] = ` @@ -509,16 +544,9 @@ exports[`PrivateKeySpeedBumpModal renders correctly 1`] = ` line-height-disabled="false" maxFontSizeMultiplier={1.2} numberOfLines={1} - onBlur={[Function]} - onFocus={[Function]} style={ { - "color": { - "dynamic": { - "dark": "#FFFFFF", - "light": "#FFFFFF", - }, - }, + "color": "#FFFFFF", "fontFamily": "Basel Grotesk", "fontSize": 17, "fontWeight": "500", diff --git a/apps/mobile/src/components/Settings/EditWalletModal/EditLabelSettingsModal.tsx b/apps/mobile/src/components/Settings/EditWalletModal/EditLabelSettingsModal.tsx index 67233dc41e3..4aff5cbda2b 100644 --- a/apps/mobile/src/components/Settings/EditWalletModal/EditLabelSettingsModal.tsx +++ b/apps/mobile/src/components/Settings/EditWalletModal/EditLabelSettingsModal.tsx @@ -105,9 +105,7 @@ export function EditLabelSettingsModal({ py="$spacing12" returnKeyType="done" value={nickname} - placeholder={ - sanitizeAddressText(shortenAddress({ address, chars: 6 })) ?? t('settings.setting.wallet.label') - } + placeholder={sanitizeAddressText(shortenAddress({ address })) ?? t('settings.setting.wallet.label')} onBlur={onFinishEditing} onChangeText={setNickname} onSubmitEditing={onFinishEditing} diff --git a/apps/mobile/src/components/TokenBalanceList/TokenBalanceList.tsx b/apps/mobile/src/components/TokenBalanceList/TokenBalanceList.tsx index bc6d6057e9c..7b350cdee9d 100644 --- a/apps/mobile/src/components/TokenBalanceList/TokenBalanceList.tsx +++ b/apps/mobile/src/components/TokenBalanceList/TokenBalanceList.tsx @@ -279,6 +279,13 @@ const TokenBalanceItemRow = memo(function TokenBalanceItemRow({ item }: { item: + navigate(ModalName.ReportTokenIssue, { + currency: portfolioBalance.currencyInfo.currency, + isMarkedSpam: portfolioBalance.currencyInfo.isSpam, + source: 'portfolio', + }) + } onPressToken={handlePressToken} > {tokenBalanceItem} diff --git a/apps/mobile/src/components/TokenDetails/TokenDetailsActionButtons.tsx b/apps/mobile/src/components/TokenDetails/TokenDetailsActionButtons.tsx index fb6b9f9ea56..75e72429278 100644 --- a/apps/mobile/src/components/TokenDetails/TokenDetailsActionButtons.tsx +++ b/apps/mobile/src/components/TokenDetails/TokenDetailsActionButtons.tsx @@ -10,6 +10,7 @@ import { TokenList } from 'uniswap/src/features/dataApi/types' import { ElementName, MobileEventName, SectionName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import Trace from 'uniswap/src/features/telemetry/Trace' +import { useShouldShowAztecWarning } from 'uniswap/src/hooks/useShouldShowAztecWarning' import { TestID, TestIDType } from 'uniswap/src/test/fixtures/testIDs' import { useBooleanState } from 'utilities/src/react/useBooleanState' @@ -66,10 +67,13 @@ export function TokenDetailsActionButtons({ }): JSX.Element { const { currencyInfo, isChainEnabled, tokenColor } = useTokenDetailsContext() const { value: actionMenuOpen, setFalse: closeActionMenu, toggle: toggleActionMenu } = useBooleanState(false) + const showAztecWarning = useShouldShowAztecWarning( + currencyInfo?.currency.isToken ? currencyInfo.currency.address : '', + ) const isBlocked = currencyInfo?.safetyInfo?.tokenList === TokenList.Blocked - const disabled = isBlocked || !isChainEnabled + const disabled = isBlocked || showAztecWarning || !isChainEnabled const validTokenColor = validColor(tokenColor) const lightTokenColor = validTokenColor ? opacify(12, validTokenColor) : undefined diff --git a/apps/mobile/src/components/TokenDetails/TokenDetailsContext.tsx b/apps/mobile/src/components/TokenDetails/TokenDetailsContext.tsx index 9f6d231631a..194ccaf059f 100644 --- a/apps/mobile/src/components/TokenDetails/TokenDetailsContext.tsx +++ b/apps/mobile/src/components/TokenDetails/TokenDetailsContext.tsx @@ -14,6 +14,7 @@ import { CurrencyField } from 'uniswap/src/types/currency' import { MobileScreens } from 'uniswap/src/types/screens/mobile' import { setClipboard } from 'uniswap/src/utils/clipboard' import { currencyIdToAddress, currencyIdToChain } from 'uniswap/src/utils/currencyId' +import { useBooleanState } from 'utilities/src/react/useBooleanState' type TokenDetailsContextState = { currencyId: string @@ -32,6 +33,9 @@ type TokenDetailsContextState = { isContractAddressExplainerModalOpen: boolean openContractAddressExplainerModal: () => void closeContractAddressExplainerModal: (markViewed: boolean) => void + isAztecWarningModalOpen: boolean + openAztecWarningModal: () => void + closeAztecWarningModal: () => void copyAddressToClipboard: (address: string) => Promise error: unknown | undefined setError: (error: unknown | undefined) => void @@ -64,6 +68,12 @@ export function TokenDetailsContextProvider({ [dispatch], ) + const { + value: isAztecWarningModalOpen, + setTrue: openAztecWarningModal, + setFalse: closeAztecWarningModal, + } = useBooleanState(false) + const copyAddressToClipboard = useCallback( async (address: string): Promise => { await setClipboard(address) @@ -113,6 +123,9 @@ export function TokenDetailsContextProvider({ isContractAddressExplainerModalOpen, openContractAddressExplainerModal, closeContractAddressExplainerModal, + isAztecWarningModalOpen, + openAztecWarningModal, + closeAztecWarningModal, copyAddressToClipboard, error, setError, @@ -121,13 +134,16 @@ export function TokenDetailsContextProvider({ activeTransactionType, closeTokenWarningModal, closeContractAddressExplainerModal, + closeAztecWarningModal, currencyId, currencyInfo, enabledChains, error, + isAztecWarningModalOpen, isContractAddressExplainerModalOpen, isTokenWarningModalOpen, navigation, + openAztecWarningModal, openContractAddressExplainerModal, openTokenWarningModal, tokenColor, diff --git a/apps/mobile/src/components/TokenDetails/TokenDetailsLinks.tsx b/apps/mobile/src/components/TokenDetails/TokenDetailsLinks.tsx index bc5f3c6e691..ac629a8be05 100644 --- a/apps/mobile/src/components/TokenDetails/TokenDetailsLinks.tsx +++ b/apps/mobile/src/components/TokenDetails/TokenDetailsLinks.tsx @@ -1,11 +1,11 @@ import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { FlatList } from 'react-native-gesture-handler' -import { getBlockExplorerIcon } from 'src/components/icons/BlockExplorerIcon' import { LinkButton, type LinkButtonProps, LinkButtonType } from 'src/components/TokenDetails/LinkButton' import { useTokenDetailsContext } from 'src/components/TokenDetails/TokenDetailsContext' import { Flex, Text } from 'ui/src' import { GlobeFilled, XTwitter } from 'ui/src/components/icons' +import { getBlockExplorerIcon } from 'uniswap/src/components/chains/BlockExplorerIcon' import { useTokenProjectUrlsPartsFragment } from 'uniswap/src/data/graphql/uniswap-data-api/fragments' import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' import { chainIdToPlatform } from 'uniswap/src/features/platforms/utils/chains' diff --git a/apps/mobile/src/components/TokenDetails/TokenDetailsStats.tsx b/apps/mobile/src/components/TokenDetails/TokenDetailsStats.tsx index be50bcd450a..5f0ccd4bbec 100644 --- a/apps/mobile/src/components/TokenDetails/TokenDetailsStats.tsx +++ b/apps/mobile/src/components/TokenDetails/TokenDetailsStats.tsx @@ -10,9 +10,8 @@ import { DEP_accentColors, validColor } from 'ui/src/theme' import { useTokenBasicInfoPartsFragment, useTokenBasicProjectPartsFragment, - useTokenMarketPartsFragment, - useTokenProjectMarketsPartsFragment, } from 'uniswap/src/data/graphql/uniswap-data-api/fragments' +import { useTokenMarketStats } from 'uniswap/src/features/dataApi/tokenDetails/useTokenDetailsData' import { currencyIdToContractInput } from 'uniswap/src/features/dataApi/utils/currencyIdToContractInput' import { Language } from 'uniswap/src/features/language/constants' import { useCurrentLanguage, useCurrentLanguageInfo } from 'uniswap/src/features/language/hooks' @@ -52,21 +51,8 @@ const TokenDetailsMarketData = memo(function _TokenDetailsMarketData(): JSX.Elem const { currencyId, tokenColor } = useTokenDetailsContext() - const tokenMarket = useTokenMarketPartsFragment({ currencyId }).data.market - const projectMarkets = useTokenProjectMarketsPartsFragment({ currencyId }).data.project?.markets - - const price = projectMarkets?.[0]?.price?.value || tokenMarket?.price?.value || undefined - const marketCap = projectMarkets?.[0]?.marketCap?.value - const volume = tokenMarket?.volume?.value - const rawPriceHigh52W = projectMarkets?.[0]?.priceHigh52W?.value || tokenMarket?.priceHigh52W?.value || undefined - const rawPriceLow52W = projectMarkets?.[0]?.priceLow52W?.value || tokenMarket?.priceLow52W?.value || undefined - - // Use current price for 52w high/low if it exceeds the bounds - const priceHight52W = - price !== undefined && rawPriceHigh52W !== undefined ? Math.max(price, rawPriceHigh52W) : rawPriceHigh52W - const priceLow52W = - price !== undefined && rawPriceLow52W !== undefined ? Math.min(price, rawPriceLow52W) : rawPriceLow52W - const fullyDilutedValuation = projectMarkets?.[0]?.fullyDilutedValuation?.value + // Use shared hook for unified data fetching (CoinGecko-first strategy) + const { marketCap, fdv, volume, high52w, low52w } = useTokenMarketStats(currencyId) return ( @@ -84,7 +70,7 @@ const TokenDetailsMarketData = memo(function _TokenDetailsMarketData(): JSX.Elem statsIcon={} > - {convertFiatAmountFormatted(fullyDilutedValuation, NumberType.FiatTokenStats)} + {convertFiatAmountFormatted(fdv, NumberType.FiatTokenStats)} @@ -102,7 +88,7 @@ const TokenDetailsMarketData = memo(function _TokenDetailsMarketData(): JSX.Elem statsIcon={} > - {convertFiatAmountFormatted(priceHight52W, NumberType.FiatTokenDetails)} + {convertFiatAmountFormatted(high52w, NumberType.FiatTokenDetails)} @@ -111,7 +97,7 @@ const TokenDetailsMarketData = memo(function _TokenDetailsMarketData(): JSX.Elem statsIcon={} > - {convertFiatAmountFormatted(priceLow52W, NumberType.FiatTokenDetails)} + {convertFiatAmountFormatted(low52w, NumberType.FiatTokenDetails)} diff --git a/apps/mobile/src/components/TokenSelector/TokenFiatOnRampList.tsx b/apps/mobile/src/components/TokenSelector/TokenFiatOnRampList.tsx index adaca05b633..6563707edd9 100644 --- a/apps/mobile/src/components/TokenSelector/TokenFiatOnRampList.tsx +++ b/apps/mobile/src/components/TokenSelector/TokenFiatOnRampList.tsx @@ -10,7 +10,8 @@ import { PortfolioBalance } from 'uniswap/src/features/dataApi/types' import { FiatOnRampCurrency, FORCurrencyOrBalance } from 'uniswap/src/features/fiatOnRamp/types' import { getUnsupportedFORTokensWithBalance, isSupportedFORCurrency } from 'uniswap/src/features/fiatOnRamp/utils' import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext' -import { useDismissedTokenWarnings } from 'uniswap/src/features/tokens/slice/hooks' +import { getTokenProtectionWarning } from 'uniswap/src/features/tokens/warnings/safetyUtils' +import { useDismissedTokenWarnings } from 'uniswap/src/features/tokens/warnings/slice/hooks' import { ListSeparatorToggle } from 'uniswap/src/features/transactions/TransactionDetails/ListSeparatorToggle' import { CurrencyId } from 'uniswap/src/types/currency' import { NumberType } from 'utilities/src/format/types' @@ -51,7 +52,8 @@ function TokenOptionItemWrapper({ [currencyInfo, balanceUSD, quantity, isUnsupported], ) const onPress = useCallback(() => onSelectCurrency(currency), [currency, onSelectCurrency]) - const { tokenWarningDismissed } = useDismissedTokenWarnings(currencyInfo?.currency) + const tokenProtectionWarning = getTokenProtectionWarning(currencyInfo) + const { tokenWarningDismissed } = useDismissedTokenWarnings(currencyInfo?.currency, tokenProtectionWarning) const { convertFiatAmountFormatted, formatNumberOrString } = useLocalizationContext() if (!option) { diff --git a/apps/mobile/src/components/accounts/AccountCardItem.tsx b/apps/mobile/src/components/accounts/AccountCardItem.tsx index 7a0547713e1..9d5cbdc115c 100644 --- a/apps/mobile/src/components/accounts/AccountCardItem.tsx +++ b/apps/mobile/src/components/accounts/AccountCardItem.tsx @@ -3,6 +3,7 @@ import React, { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import ContextMenu from 'react-native-context-menu-view' import { useDispatch } from 'react-redux' +import { MODAL_OPEN_WAIT_TIME } from 'src/app/navigation/constants' import { navigate } from 'src/app/navigation/rootNavigation' import { NotificationBadge } from 'src/components/notifications/Badge' import { Flex, Text, TouchableArea } from 'ui/src' @@ -24,8 +25,6 @@ import { noop } from 'utilities/src/react/noop' import { useAccountListData } from 'wallet/src/features/accounts/useAccountListData' import { useAccounts } from 'wallet/src/features/wallet/hooks' -const MODAL_CLOSE_WAIT_TIME = 300 - type AccountCardItemProps = { address: Address isViewOnly: boolean @@ -130,7 +129,7 @@ export function AccountCardItem({ navigate(ModalName.ConnectionsDappListModal, { address, }) - }, MODAL_CLOSE_WAIT_TIME) + }, MODAL_OPEN_WAIT_TIME) }, [address, onClose]) const onPressRemoveWallet = useCallback(() => { diff --git a/apps/mobile/src/components/accounts/__snapshots__/AccountCardItem.test.tsx.snap b/apps/mobile/src/components/accounts/__snapshots__/AccountCardItem.test.tsx.snap index 5727f60c85c..15e2ee463fd 100644 --- a/apps/mobile/src/components/accounts/__snapshots__/AccountCardItem.test.tsx.snap +++ b/apps/mobile/src/components/accounts/__snapshots__/AccountCardItem.test.tsx.snap @@ -17,11 +17,38 @@ exports[`AccountCardItem renders correctly 1`] = ` collapsable={false} focusVisibleStyle={{}} forwardedRef={[Function]} + jestAnimatedProps={ + { + "value": {}, + } + } jestAnimatedStyle={ { "value": {}, } } + jestInlineStyle={ + [ + { + "backgroundColor": "transparent", + "borderBottomLeftRadius": 12, + "borderBottomRightRadius": 12, + "borderTopLeftRadius": 12, + "borderTopRightRadius": 12, + "flexDirection": "column", + "opacity": 1, + "paddingBottom": 12, + "paddingLeft": 24, + "paddingRight": 24, + "paddingTop": 8, + "transform": [ + { + "scale": 1, + }, + ], + }, + ] + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -34,29 +61,30 @@ exports[`AccountCardItem renders correctly 1`] = ` onStartShouldSetResponder={[Function]} role="button" style={ - { - "backgroundColor": "transparent", - "borderBottomLeftRadius": 12, - "borderBottomRightRadius": 12, - "borderTopLeftRadius": 12, - "borderTopRightRadius": 12, - "flexDirection": "column", - "opacity": 1, - "paddingBottom": 12, - "paddingLeft": 24, - "paddingRight": 24, - "paddingTop": 8, - "transform": [ - { - "scale": 1, - }, - ], - } + [ + { + "backgroundColor": "transparent", + "borderBottomLeftRadius": 12, + "borderBottomRightRadius": 12, + "borderTopLeftRadius": 12, + "borderTopRightRadius": 12, + "flexDirection": "column", + "opacity": 1, + "paddingBottom": 12, + "paddingLeft": 24, + "paddingRight": 24, + "paddingTop": 8, + "transform": [ + { + "scale": 1, + }, + ], + }, + {}, + ] } > diff --git a/apps/mobile/src/components/accounts/__snapshots__/AccountList.test.tsx.snap b/apps/mobile/src/components/accounts/__snapshots__/AccountList.test.tsx.snap index 732b9a75f9f..51ede447887 100644 --- a/apps/mobile/src/components/accounts/__snapshots__/AccountList.test.tsx.snap +++ b/apps/mobile/src/components/accounts/__snapshots__/AccountList.test.tsx.snap @@ -113,8 +113,6 @@ exports[`AccountList renders without error 1`] = ` } > diff --git a/apps/mobile/src/components/education/SeedPhrase.tsx b/apps/mobile/src/components/education/SeedPhrase.tsx index 91ccad3d526..a0d70799b98 100644 --- a/apps/mobile/src/components/education/SeedPhrase.tsx +++ b/apps/mobile/src/components/education/SeedPhrase.tsx @@ -62,25 +62,60 @@ function Page({ text, params }: { text: ReactNode; params: OnboardingStackBasePa ) } -export const SeedPhraseEducationContent = (params: OnboardingStackBaseParams): JSX.Element[] => { - const cloudProviderName = getCloudProviderName() - const highlightComponent = +const highlightComponent = + +const cloudProviderName = getCloudProviderName() - const pageContentList = [ - // biome-ignore-start lint/correctness/useJsxKeyInIterable: Static array items don't need keys - , - , - , - , - , - , - // biome-ignore-end lint/correctness/useJsxKeyInIterable: Static array items don't need keys - ] +const pageContentList = [ + , + , + , + , + , + , + , + , + , +] +export const SeedPhraseEducationContent = (params: OnboardingStackBaseParams): JSX.Element[] => { return pageContentList.map((content, i) => ( {content}} /> )) diff --git a/apps/mobile/src/components/explore/ExploreSections/ExploreSections.tsx b/apps/mobile/src/components/explore/ExploreSections/ExploreSections.tsx index edda8bccac5..5dff1928fec 100644 --- a/apps/mobile/src/components/explore/ExploreSections/ExploreSections.tsx +++ b/apps/mobile/src/components/explore/ExploreSections/ExploreSections.tsx @@ -30,6 +30,7 @@ import { TokenItemData } from 'src/components/explore/TokenItemData' import { getTokenMetadataDisplayType } from 'src/features/explore/utils' import { Flex, Loader, Text } from 'ui/src' import { AnimatedBottomSheetFlashList } from 'ui/src/components/AnimatedFlashList/AnimatedFlashList' +import { NoTokens } from 'ui/src/components/icons' import { spacing } from 'ui/src/theme' import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard' import { useTokenRankingsQuery } from 'uniswap/src/data/rest/tokenRankings' @@ -154,10 +155,15 @@ function _ExploreSections({ }, [insets.bottom]) const dataWithBottomTabs = useMemo( - () => (showFullScreenLoadingState ? [] : (topTokenItems ?? [])), + () => (showFullScreenLoadingState ? [] : topTokenItems), [showFullScreenLoadingState, topTokenItems], ) + const listEmptyComponent = useMemo( + () => , + [showFullScreenLoadingState], + ) + if (!hasAllData && error) { return ( @@ -176,7 +182,7 @@ function _ExploreSections({ { +): Partial> { if (!tokenRankings) { - return {} as Record + return {} as const } const result: Record = {} @@ -321,14 +327,11 @@ function processTokenRankings( return result } -function useTokenItems( - data: TokenRankingsResponse | undefined, - orderBy: ExploreOrderBy, -): TokenItemDataWithMetadata[] | undefined { +function useTokenItems(data: TokenRankingsResponse | undefined, orderBy: ExploreOrderBy): TokenItemDataWithMetadata[] { // process all the token rankings into a map of orderBy to token items (only do this once) const allTokenItemsByOrderBy = useMemo(() => processTokenRankings(data?.tokenRankings), [data]) - // return the token items for the given orderBy - return useMemo(() => allTokenItemsByOrderBy[orderBy], [allTokenItemsByOrderBy, orderBy]) + // return the token items for the given orderBy, or empty array if the orderBy key doesn't exist + return useMemo(() => allTokenItemsByOrderBy[orderBy] ?? [], [allTokenItemsByOrderBy, orderBy]) } type ListHeaderProps = { @@ -386,11 +389,31 @@ const ListHeaderComponent = ({ ) } -const ListEmptyComponent = (): JSX.Element => ( - - - -) +const TokenListEmptyComponent = memo(function TokenListEmptyComponent({ + isLoading, +}: { + isLoading: boolean +}): JSX.Element { + const { t } = useTranslation() + + if (isLoading) { + return ( + + + + ) + } + + return ( + + } + title={t('explore.tokens.empty.title')} + /> + + ) +}) function useOrderBy(): { uiOrderBy: ExploreOrderBy diff --git a/apps/mobile/src/components/explore/ExploreSections/NetworkPillsRow.tsx b/apps/mobile/src/components/explore/ExploreSections/NetworkPillsRow.tsx index b0383bd3525..425f19a2438 100644 --- a/apps/mobile/src/components/explore/ExploreSections/NetworkPillsRow.tsx +++ b/apps/mobile/src/components/explore/ExploreSections/NetworkPillsRow.tsx @@ -1,3 +1,4 @@ +import { useTheme } from '@react-navigation/core' import { memo, useCallback, useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' import { ViewStyle } from 'react-native' @@ -58,6 +59,7 @@ const NetworkPillsRow = memo(function NetworkPillsRow({ onSelectNetwork: (chainId: UniverseChainId | null) => void }): JSX.Element { const colors = useSporeColors() + const theme = useTheme() const { chains } = useEnabledChains() const flatListRef = useRef>(null) @@ -68,6 +70,7 @@ const NetworkPillsRow = memo(function NetworkPillsRow({ items: chains, }) + // biome-ignore lint/correctness/useExhaustiveDependencies: need theme dep for foregroundColor to change on theme change const renderItemNetworkPills = useCallback( ({ item }: { item: UniverseChainId }) => { return ( @@ -90,7 +93,7 @@ const NetworkPillsRow = memo(function NetworkPillsRow({ ) }, - [colors.neutral1.val, onSelectNetwork, selectedNetwork], + [colors.neutral1.val, onSelectNetwork, selectedNetwork, theme], ) const ListHeaderComponent = useMemo(() => { diff --git a/apps/mobile/src/components/explore/__snapshots__/FavoriteHeaderRow.test.tsx.snap b/apps/mobile/src/components/explore/__snapshots__/FavoriteHeaderRow.test.tsx.snap index 2217f92f48c..da2b061869e 100644 --- a/apps/mobile/src/components/explore/__snapshots__/FavoriteHeaderRow.test.tsx.snap +++ b/apps/mobile/src/components/explore/__snapshots__/FavoriteHeaderRow.test.tsx.snap @@ -20,12 +20,7 @@ exports[`FavoriteHeaderRow when editing renders without error 1`] = ` maxFontSizeMultiplier={1.4} style={ { - "color": { - "dynamic": { - "dark": "rgba(255, 255, 255, 0.65)", - "light": "rgba(19, 19, 19, 0.63)", - }, - }, + "color": "rgba(19, 19, 19, 0.63)", "fontFamily": "Basel Grotesk", "fontSize": 17, "fontWeight": "400", @@ -41,11 +36,34 @@ exports[`FavoriteHeaderRow when editing renders without error 1`] = ` focusVisibleStyle={{}} forwardedRef={[Function]} hitSlop={16} + jestAnimatedProps={ + { + "value": {}, + } + } jestAnimatedStyle={ { "value": {}, } } + jestInlineStyle={ + [ + { + "backgroundColor": "transparent", + "borderBottomLeftRadius": 12, + "borderBottomRightRadius": 12, + "borderTopLeftRadius": 12, + "borderTopRightRadius": 12, + "flexDirection": "column", + "opacity": 1, + "transform": [ + { + "scale": 1, + }, + ], + }, + ] + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -58,35 +76,31 @@ exports[`FavoriteHeaderRow when editing renders without error 1`] = ` onStartShouldSetResponder={[Function]} role="button" style={ - { - "backgroundColor": "transparent", - "borderBottomLeftRadius": 12, - "borderBottomRightRadius": 12, - "borderTopLeftRadius": 12, - "borderTopRightRadius": 12, - "flexDirection": "column", - "opacity": 1, - "transform": [ - { - "scale": 1, - }, - ], - } + [ + { + "backgroundColor": "transparent", + "borderBottomLeftRadius": 12, + "borderBottomRightRadius": 12, + "borderTopLeftRadius": 12, + "borderTopRightRadius": 12, + "flexDirection": "column", + "opacity": 1, + "transform": [ + { + "scale": 1, + }, + ], + }, + {}, + ] } > diff --git a/apps/mobile/src/components/explore/__snapshots__/FavoriteTokenCard.test.tsx.snap b/apps/mobile/src/components/explore/__snapshots__/FavoriteTokenCard.test.tsx.snap index f6d96f05640..41fc51fc1c9 100644 --- a/apps/mobile/src/components/explore/__snapshots__/FavoriteTokenCard.test.tsx.snap +++ b/apps/mobile/src/components/explore/__snapshots__/FavoriteTokenCard.test.tsx.snap @@ -38,11 +38,51 @@ exports[`FavoriteTokenCard renders without error 1`] = ` collapsable={false} focusVisibleStyle={{}} forwardedRef={[Function]} + jestAnimatedProps={ + { + "value": {}, + } + } jestAnimatedStyle={ { "value": {}, } } + jestInlineStyle={ + [ + { + "backgroundColor": "#FFFFFF", + "borderBottomColor": "rgba(19, 19, 19, 0.08)", + "borderBottomLeftRadius": 16, + "borderBottomRightRadius": 16, + "borderBottomWidth": 1, + "borderLeftColor": "rgba(19, 19, 19, 0.08)", + "borderLeftWidth": 1, + "borderRightColor": "rgba(19, 19, 19, 0.08)", + "borderRightWidth": 1, + "borderStyle": "solid", + "borderTopColor": "rgba(19, 19, 19, 0.08)", + "borderTopLeftRadius": 16, + "borderTopRightRadius": 16, + "borderTopWidth": 1, + "flexDirection": "column", + "opacity": 1, + "overflow": "hidden", + "shadowColor": "rgb(0,0,0)", + "shadowOffset": { + "height": 1, + "width": 0, + }, + "shadowOpacity": 0.0196078431372549, + "shadowRadius": 6, + "transform": [ + { + "scale": 1, + }, + ], + }, + ] + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -55,43 +95,44 @@ exports[`FavoriteTokenCard renders without error 1`] = ` onStartShouldSetResponder={[Function]} role="button" style={ - { - "backgroundColor": "#FFFFFF", - "borderBottomColor": "rgba(19, 19, 19, 0.08)", - "borderBottomLeftRadius": 16, - "borderBottomRightRadius": 16, - "borderBottomWidth": 1, - "borderLeftColor": "rgba(19, 19, 19, 0.08)", - "borderLeftWidth": 1, - "borderRightColor": "rgba(19, 19, 19, 0.08)", - "borderRightWidth": 1, - "borderStyle": "solid", - "borderTopColor": "rgba(19, 19, 19, 0.08)", - "borderTopLeftRadius": 16, - "borderTopRightRadius": 16, - "borderTopWidth": 1, - "flexDirection": "column", - "opacity": 1, - "overflow": "hidden", - "shadowColor": "rgb(0,0,0)", - "shadowOffset": { - "height": 1, - "width": 0, - }, - "shadowOpacity": 0.0196078431372549, - "shadowRadius": 6, - "transform": [ - { - "scale": 1, + [ + { + "backgroundColor": "#FFFFFF", + "borderBottomColor": "rgba(19, 19, 19, 0.08)", + "borderBottomLeftRadius": 16, + "borderBottomRightRadius": 16, + "borderBottomWidth": 1, + "borderLeftColor": "rgba(19, 19, 19, 0.08)", + "borderLeftWidth": 1, + "borderRightColor": "rgba(19, 19, 19, 0.08)", + "borderRightWidth": 1, + "borderStyle": "solid", + "borderTopColor": "rgba(19, 19, 19, 0.08)", + "borderTopLeftRadius": 16, + "borderTopRightRadius": 16, + "borderTopWidth": 1, + "flexDirection": "column", + "opacity": 1, + "overflow": "hidden", + "shadowColor": "rgb(0,0,0)", + "shadowOffset": { + "height": 1, + "width": 0, }, - ], - } + "shadowOpacity": 0.0196078431372549, + "shadowRadius": 6, + "transform": [ + { + "scale": 1, + }, + ], + }, + {}, + ] } testID="favorite-token-card-undefined" > { - flatListRef: React.RefObject> + flatListRef: React.RefObject | null> selectedItem: T | null items: T[] scrollDelay?: number @@ -20,7 +20,7 @@ export function useFlatListAutoScroll(options: UseFlatListAutoScrollOptions { - let timeoutId: NodeJS.Timeout | undefined + let timeoutId: NodeJS.Timeout | number | undefined if (flatListRef.current) { // If selectedItem is null (All/First option), scroll to the beginning diff --git a/apps/mobile/src/components/explore/search/SearchPopularNFTCollections.graphql b/apps/mobile/src/components/explore/search/SearchPopularNFTCollections.graphql deleted file mode 100644 index 61455310ac7..00000000000 --- a/apps/mobile/src/components/explore/search/SearchPopularNFTCollections.graphql +++ /dev/null @@ -1,21 +0,0 @@ -query SearchPopularNFTCollections { - topCollections(chains: [ETHEREUM], orderBy: VOLUME, duration: DAY, first: 2) { - edges { - node { - id - name - collectionId - isVerified - nftContracts { - id - chain - address - } - image { - id - url - } - } - } - } -} diff --git a/apps/mobile/src/components/fiatOnRamp/CtaButton.tsx b/apps/mobile/src/components/fiatOnRamp/CtaButton.tsx index b6285af3438..a4f042361c5 100644 --- a/apps/mobile/src/components/fiatOnRamp/CtaButton.tsx +++ b/apps/mobile/src/components/fiatOnRamp/CtaButton.tsx @@ -1,7 +1,7 @@ import React from 'react' import { useTranslation } from 'react-i18next' import { Button, SpinningLoader, useIsShortMobileDevice } from 'ui/src' -import { InfoCircleFilled } from 'ui/src/components/icons' +import { InfoCircleFilled } from 'ui/src/components/icons/InfoCircleFilled' interface FiatOnRampCtaButtonProps { onPress: () => void diff --git a/apps/mobile/src/components/icons/TripleDot.tsx b/apps/mobile/src/components/icons/TripleDot.tsx deleted file mode 100644 index 32488c129fe..00000000000 --- a/apps/mobile/src/components/icons/TripleDot.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React, { memo } from 'react' -import { ColorTokens, Flex } from 'ui/src' - -type Props = { - size?: number - color?: ColorTokens -} - -export const TripleDot = memo(function _TripleDot({ size = 5, color = '$neutral2' }: Props) { - return ( - - - - - - ) -}) diff --git a/apps/mobile/src/components/input/__snapshots__/SelectionCircle.test.tsx.snap b/apps/mobile/src/components/input/__snapshots__/SelectionCircle.test.tsx.snap index f1d13ae2006..b29ce1681af 100644 --- a/apps/mobile/src/components/input/__snapshots__/SelectionCircle.test.tsx.snap +++ b/apps/mobile/src/components/input/__snapshots__/SelectionCircle.test.tsx.snap @@ -5,36 +5,16 @@ exports[`renders selection circle 1`] = ` style={ { "alignItems": "center", - "borderBottomColor": { - "dynamic": { - "dark": "#FF37C7", - "light": "#FF37C7", - }, - }, + "borderBottomColor": "#FF37C7", "borderBottomLeftRadius": 999999, "borderBottomRightRadius": 999999, "borderBottomWidth": 1, - "borderLeftColor": { - "dynamic": { - "dark": "#FF37C7", - "light": "#FF37C7", - }, - }, + "borderLeftColor": "#FF37C7", "borderLeftWidth": 1, - "borderRightColor": { - "dynamic": { - "dark": "#FF37C7", - "light": "#FF37C7", - }, - }, + "borderRightColor": "#FF37C7", "borderRightWidth": 1, "borderStyle": "solid", - "borderTopColor": { - "dynamic": { - "dark": "#FF37C7", - "light": "#FF37C7", - }, - }, + "borderTopColor": "#FF37C7", "borderTopLeftRadius": 999999, "borderTopRightRadius": 999999, "borderTopWidth": 1, @@ -48,12 +28,7 @@ exports[`renders selection circle 1`] = ` | RefObject> + list: RefObject | RefObject | null> position: Animated.SharedValue index: number } diff --git a/apps/mobile/src/components/layout/screens/HeaderScrollScreen.tsx b/apps/mobile/src/components/layout/screens/HeaderScrollScreen.tsx index 3aac1094369..955b828c3c2 100644 --- a/apps/mobile/src/components/layout/screens/HeaderScrollScreen.tsx +++ b/apps/mobile/src/components/layout/screens/HeaderScrollScreen.tsx @@ -19,8 +19,6 @@ const EDGES: Edge[] = ['top', 'left', 'right'] type HeaderScrollScreenProps = { centerElement?: JSX.Element rightElement?: JSX.Element - alwaysShowCenterElement?: boolean - fullScreen?: boolean // Expand to device edges renderedInModal?: boolean // Apply styling to display within bottom sheet modal showHandleBar?: boolean // add handlebar element to top of view backgroundColor?: ColorTokens @@ -30,8 +28,6 @@ type HeaderScrollScreenProps = { export function HeaderScrollScreen({ centerElement, rightElement = , - alwaysShowCenterElement, - fullScreen = false, renderedInModal = false, showHandleBar = false, backgroundColor = '$surface1', @@ -63,14 +59,12 @@ export function HeaderScrollScreen({ ) return ( - + {showHandleBar ? : null} showHeaderScrollYDistance: number + fullScreen?: boolean // hard to type // biome-ignore lint/suspicious/noExplicitAny: Ref type varies based on list component used listRef: React.MutableRefObject centerElement?: JSX.Element rightElement?: JSX.Element - alwaysShowCenterElement?: boolean - fullScreen?: boolean // Expand to device edges backgroundColor?: ColorTokens backButtonColor?: ColorTokens } @@ -36,7 +35,6 @@ export function ScrollHeader({ showHeaderScrollYDistance, centerElement, rightElement = , - alwaysShowCenterElement, fullScreen = false, backgroundColor, backButtonColor, @@ -74,11 +72,7 @@ export function ScrollHeader({ > - {alwaysShowCenterElement ? ( - centerElement - ) : ( - {centerElement} - )} + {centerElement} {rightElement} diff --git a/apps/mobile/src/components/modals/ReactNavigationModals/ReactNavigationModal.tsx b/apps/mobile/src/components/modals/ReactNavigationModals/ReactNavigationModal.tsx index 64cbdf8aac4..7e8b9ad958b 100644 --- a/apps/mobile/src/components/modals/ReactNavigationModals/ReactNavigationModal.tsx +++ b/apps/mobile/src/components/modals/ReactNavigationModals/ReactNavigationModal.tsx @@ -4,6 +4,8 @@ import { useReactNavigationModal } from 'src/components/modals/useReactNavigatio import type { GetProps } from 'ui/src' import { BridgedAssetModal } from 'uniswap/src/components/BridgedAsset/BridgedAssetModal' import { WormholeModal } from 'uniswap/src/components/BridgedAsset/WormholeModal' +import { ReportTokenDataModal } from 'uniswap/src/components/reporting/ReportTokenDataModal' +import { ReportTokenIssueModal } from 'uniswap/src/components/reporting/ReportTokenIssueModal' import { PasskeyManagementModal } from 'uniswap/src/features/passkey/PasskeyManagementModal' import { PasskeysHelpModal } from 'uniswap/src/features/passkey/PasskeysHelpModal' import { ModalName } from 'uniswap/src/features/telemetry/constants' @@ -31,6 +33,8 @@ type ValidModalNames = keyof Pick< | typeof ModalName.LanguageSelector | typeof ModalName.BridgedAsset | typeof ModalName.Wormhole + | typeof ModalName.ReportTokenIssue + | typeof ModalName.ReportTokenData > type ModalNameWithComponentProps = { @@ -46,6 +50,8 @@ type ModalNameWithComponentProps = { [ModalName.LanguageSelector]: GetProps [ModalName.BridgedAsset]: GetProps [ModalName.Wormhole]: GetProps + [ModalName.ReportTokenIssue]: GetProps + [ModalName.ReportTokenData]: GetProps } type NavigationModalProps = { diff --git a/apps/mobile/src/components/modals/ReactNavigationModals/ReportTokenDataModalScreen.tsx b/apps/mobile/src/components/modals/ReactNavigationModals/ReportTokenDataModalScreen.tsx new file mode 100644 index 00000000000..0cf6770c2b4 --- /dev/null +++ b/apps/mobile/src/components/modals/ReactNavigationModals/ReportTokenDataModalScreen.tsx @@ -0,0 +1,10 @@ +import { AppStackScreenProp } from 'src/app/navigation/types' +import { ReactNavigationModal } from 'src/components/modals/ReactNavigationModals/ReactNavigationModal' +import { ReportTokenDataModal } from 'uniswap/src/components/reporting/ReportTokenDataModal' +import { ModalName } from 'uniswap/src/features/telemetry/constants' + +export const ReportTokenDataModalScreen = ( + props: AppStackScreenProp, +): JSX.Element => { + return +} diff --git a/apps/mobile/src/components/modals/ReactNavigationModals/ReportTokenIssueModalScreen.tsx b/apps/mobile/src/components/modals/ReactNavigationModals/ReportTokenIssueModalScreen.tsx new file mode 100644 index 00000000000..aba9bc3f71a --- /dev/null +++ b/apps/mobile/src/components/modals/ReactNavigationModals/ReportTokenIssueModalScreen.tsx @@ -0,0 +1,10 @@ +import { AppStackScreenProp } from 'src/app/navigation/types' +import { ReactNavigationModal } from 'src/components/modals/ReactNavigationModals/ReactNavigationModal' +import { ReportTokenIssueModal } from 'uniswap/src/components/reporting/ReportTokenIssueModal' +import { ModalName } from 'uniswap/src/features/telemetry/constants' + +export const ReportTokenIssueModalScreen = ( + props: AppStackScreenProp, +): JSX.Element => { + return +} diff --git a/apps/mobile/src/components/text/LongMarkdownText.tsx b/apps/mobile/src/components/text/LongMarkdownText.tsx index 27f4f51fdd5..d58333fc546 100644 --- a/apps/mobile/src/components/text/LongMarkdownText.tsx +++ b/apps/mobile/src/components/text/LongMarkdownText.tsx @@ -35,7 +35,7 @@ export function LongMarkdownText(props: LongMarkdownTextProps): JSX.Element { const [expanded, toggleExpanded] = useReducer((isExpanded) => !isExpanded, true) const [textLengthExceedsLimit, setTextLengthExceedsLimit] = useState(false) const [textLineHeight, setTextLineHeight] = useState(fonts[variant].lineHeight) - const initialContentHeightRef = useRef() + const initialContentHeightRef = useRef(undefined) const maxVisibleHeight = textLineHeight * initialDisplayedLines const onMarkdownLayout = useCallback( diff --git a/apps/mobile/src/components/text/__snapshots__/AnimatedText.test.tsx.snap b/apps/mobile/src/components/text/__snapshots__/AnimatedText.test.tsx.snap index eeceb546c5f..a95e4887071 100644 --- a/apps/mobile/src/components/text/__snapshots__/AnimatedText.test.tsx.snap +++ b/apps/mobile/src/components/text/__snapshots__/AnimatedText.test.tsx.snap @@ -5,20 +5,46 @@ exports[`AnimatedText renders without error 1`] = ` allowFontScaling={true} collapsable={false} editable={false} + jestAnimatedProps={ + { + "value": { + "text": "Rendered", + }, + } + } jestAnimatedStyle={ { "value": {}, } } + jestInlineStyle={ + [ + { + "padding": 0, + }, + { + "fontFamily": "Basel Grotesk", + "fontSize": 17, + "fontWeight": "400", + "lineHeight": 22.1, + }, + undefined, + ] + } maxFontSizeMultiplier={1.4} style={ - { - "fontFamily": "Basel Grotesk", - "fontSize": 17, - "fontWeight": "400", - "lineHeight": 22.1, - "padding": 0, - } + [ + { + "padding": 0, + }, + { + "fontFamily": "Basel Grotesk", + "fontSize": 17, + "fontWeight": "400", + "lineHeight": 22.1, + }, + undefined, + ] } text="Rendered" underlineColorAndroid="transparent" diff --git a/apps/mobile/src/components/text/__snapshots__/DecimalNumber.test.tsx.snap b/apps/mobile/src/components/text/__snapshots__/DecimalNumber.test.tsx.snap index 9e4ebfa46c2..7def4eb8b01 100644 --- a/apps/mobile/src/components/text/__snapshots__/DecimalNumber.test.tsx.snap +++ b/apps/mobile/src/components/text/__snapshots__/DecimalNumber.test.tsx.snap @@ -6,12 +6,7 @@ exports[`renders a DecimalNumber 1`] = ` maxFontSizeMultiplier={1.4} style={ { - "color": { - "dynamic": { - "dark": "#FFFFFF", - "light": "#131313", - }, - }, + "color": "#131313", "fontFamily": "Basel Grotesk", "fontSize": 19, "fontWeight": "400", @@ -26,12 +21,7 @@ exports[`renders a DecimalNumber 1`] = ` maxFontSizeMultiplier={1.4} style={ { - "color": { - "dynamic": { - "dark": "rgba(255, 255, 255, 0.38)", - "light": "rgba(19, 19, 19, 0.35)", - }, - }, + "color": "rgba(19, 19, 19, 0.35)", "fontFamily": "Basel Grotesk", "fontSize": 19, "fontWeight": "400", @@ -52,12 +42,7 @@ exports[`renders a DecimalNumber without a comma separator 1`] = ` maxFontSizeMultiplier={1.4} style={ { - "color": { - "dynamic": { - "dark": "#FFFFFF", - "light": "#131313", - }, - }, + "color": "#131313", "fontFamily": "Basel Grotesk", "fontSize": 19, "fontWeight": "400", @@ -72,12 +57,7 @@ exports[`renders a DecimalNumber without a comma separator 1`] = ` maxFontSizeMultiplier={1.4} style={ { - "color": { - "dynamic": { - "dark": "rgba(255, 255, 255, 0.38)", - "light": "rgba(19, 19, 19, 0.35)", - }, - }, + "color": "rgba(19, 19, 19, 0.35)", "fontFamily": "Basel Grotesk", "fontSize": 19, "fontWeight": "400", @@ -98,12 +78,7 @@ exports[`renders a DecimalNumber without a decimal part 1`] = ` maxFontSizeMultiplier={1.4} style={ { - "color": { - "dynamic": { - "dark": "#FFFFFF", - "light": "#131313", - }, - }, + "color": "#131313", "fontFamily": "Basel Grotesk", "fontSize": 19, "fontWeight": "400", diff --git a/apps/mobile/src/components/text/__snapshots__/TextWithFuseMatches.test.tsx.snap b/apps/mobile/src/components/text/__snapshots__/TextWithFuseMatches.test.tsx.snap index ea3c13571bf..f5750a3f748 100644 --- a/apps/mobile/src/components/text/__snapshots__/TextWithFuseMatches.test.tsx.snap +++ b/apps/mobile/src/components/text/__snapshots__/TextWithFuseMatches.test.tsx.snap @@ -14,12 +14,7 @@ exports[`renders text with few matches 1`] = ` maxFontSizeMultiplier={1.4} style={ { - "color": { - "dynamic": { - "dark": "#FFFFFF", - "light": "#131313", - }, - }, + "color": "#131313", "fontFamily": "Basel Grotesk", "fontSize": 19, "fontWeight": "400", @@ -35,12 +30,7 @@ exports[`renders text with few matches 1`] = ` maxFontSizeMultiplier={1.4} style={ { - "color": { - "dynamic": { - "dark": "#FFFFFF", - "light": "#131313", - }, - }, + "color": "#131313", "fontFamily": "Basel Grotesk", "fontSize": 19, "fontWeight": "400", @@ -56,12 +46,7 @@ exports[`renders text with few matches 1`] = ` maxFontSizeMultiplier={1.4} style={ { - "color": { - "dynamic": { - "dark": "rgba(255, 255, 255, 0.38)", - "light": "rgba(19, 19, 19, 0.35)", - }, - }, + "color": "rgba(19, 19, 19, 0.35)", "fontFamily": "Basel Grotesk", "fontSize": 19, "fontWeight": "400", @@ -77,12 +62,7 @@ exports[`renders text with few matches 1`] = ` maxFontSizeMultiplier={1.4} style={ { - "color": { - "dynamic": { - "dark": "rgba(255, 255, 255, 0.38)", - "light": "rgba(19, 19, 19, 0.35)", - }, - }, + "color": "rgba(19, 19, 19, 0.35)", "fontFamily": "Basel Grotesk", "fontSize": 19, "fontWeight": "400", @@ -98,12 +78,7 @@ exports[`renders text with few matches 1`] = ` maxFontSizeMultiplier={1.4} style={ { - "color": { - "dynamic": { - "dark": "#FFFFFF", - "light": "#131313", - }, - }, + "color": "#131313", "fontFamily": "Basel Grotesk", "fontSize": 19, "fontWeight": "400", @@ -119,12 +94,7 @@ exports[`renders text with few matches 1`] = ` maxFontSizeMultiplier={1.4} style={ { - "color": { - "dynamic": { - "dark": "#FFFFFF", - "light": "#131313", - }, - }, + "color": "#131313", "fontFamily": "Basel Grotesk", "fontSize": 19, "fontWeight": "400", @@ -140,12 +110,7 @@ exports[`renders text with few matches 1`] = ` maxFontSizeMultiplier={1.4} style={ { - "color": { - "dynamic": { - "dark": "#FFFFFF", - "light": "#131313", - }, - }, + "color": "#131313", "fontFamily": "Basel Grotesk", "fontSize": 19, "fontWeight": "400", @@ -161,12 +126,7 @@ exports[`renders text with few matches 1`] = ` maxFontSizeMultiplier={1.4} style={ { - "color": { - "dynamic": { - "dark": "#FFFFFF", - "light": "#131313", - }, - }, + "color": "#131313", "fontFamily": "Basel Grotesk", "fontSize": 19, "fontWeight": "400", @@ -182,12 +142,7 @@ exports[`renders text with few matches 1`] = ` maxFontSizeMultiplier={1.4} style={ { - "color": { - "dynamic": { - "dark": "#FFFFFF", - "light": "#131313", - }, - }, + "color": "#131313", "fontFamily": "Basel Grotesk", "fontSize": 19, "fontWeight": "400", @@ -203,12 +158,7 @@ exports[`renders text with few matches 1`] = ` maxFontSizeMultiplier={1.4} style={ { - "color": { - "dynamic": { - "dark": "rgba(255, 255, 255, 0.38)", - "light": "rgba(19, 19, 19, 0.35)", - }, - }, + "color": "rgba(19, 19, 19, 0.35)", "fontFamily": "Basel Grotesk", "fontSize": 19, "fontWeight": "400", @@ -224,12 +174,7 @@ exports[`renders text with few matches 1`] = ` maxFontSizeMultiplier={1.4} style={ { - "color": { - "dynamic": { - "dark": "rgba(255, 255, 255, 0.38)", - "light": "rgba(19, 19, 19, 0.35)", - }, - }, + "color": "rgba(19, 19, 19, 0.35)", "fontFamily": "Basel Grotesk", "fontSize": 19, "fontWeight": "400", @@ -245,12 +190,7 @@ exports[`renders text with few matches 1`] = ` maxFontSizeMultiplier={1.4} style={ { - "color": { - "dynamic": { - "dark": "rgba(255, 255, 255, 0.38)", - "light": "rgba(19, 19, 19, 0.35)", - }, - }, + "color": "rgba(19, 19, 19, 0.35)", "fontFamily": "Basel Grotesk", "fontSize": 19, "fontWeight": "400", @@ -266,12 +206,7 @@ exports[`renders text with few matches 1`] = ` maxFontSizeMultiplier={1.4} style={ { - "color": { - "dynamic": { - "dark": "rgba(255, 255, 255, 0.38)", - "light": "rgba(19, 19, 19, 0.35)", - }, - }, + "color": "rgba(19, 19, 19, 0.35)", "fontFamily": "Basel Grotesk", "fontSize": 19, "fontWeight": "400", @@ -287,12 +222,7 @@ exports[`renders text with few matches 1`] = ` maxFontSizeMultiplier={1.4} style={ { - "color": { - "dynamic": { - "dark": "rgba(255, 255, 255, 0.38)", - "light": "rgba(19, 19, 19, 0.35)", - }, - }, + "color": "rgba(19, 19, 19, 0.35)", "fontFamily": "Basel Grotesk", "fontSize": 19, "fontWeight": "400", @@ -308,12 +238,7 @@ exports[`renders text with few matches 1`] = ` maxFontSizeMultiplier={1.4} style={ { - "color": { - "dynamic": { - "dark": "rgba(255, 255, 255, 0.38)", - "light": "rgba(19, 19, 19, 0.35)", - }, - }, + "color": "rgba(19, 19, 19, 0.35)", "fontFamily": "Basel Grotesk", "fontSize": 19, "fontWeight": "400", @@ -329,12 +254,7 @@ exports[`renders text with few matches 1`] = ` maxFontSizeMultiplier={1.4} style={ { - "color": { - "dynamic": { - "dark": "rgba(255, 255, 255, 0.38)", - "light": "rgba(19, 19, 19, 0.35)", - }, - }, + "color": "rgba(19, 19, 19, 0.35)", "fontFamily": "Basel Grotesk", "fontSize": 19, "fontWeight": "400", @@ -350,12 +270,7 @@ exports[`renders text with few matches 1`] = ` maxFontSizeMultiplier={1.4} style={ { - "color": { - "dynamic": { - "dark": "rgba(255, 255, 255, 0.38)", - "light": "rgba(19, 19, 19, 0.35)", - }, - }, + "color": "rgba(19, 19, 19, 0.35)", "fontFamily": "Basel Grotesk", "fontSize": 19, "fontWeight": "400", @@ -371,12 +286,7 @@ exports[`renders text with few matches 1`] = ` maxFontSizeMultiplier={1.4} style={ { - "color": { - "dynamic": { - "dark": "rgba(255, 255, 255, 0.38)", - "light": "rgba(19, 19, 19, 0.35)", - }, - }, + "color": "rgba(19, 19, 19, 0.35)", "fontFamily": "Basel Grotesk", "fontSize": 19, "fontWeight": "400", @@ -392,12 +302,7 @@ exports[`renders text with few matches 1`] = ` maxFontSizeMultiplier={1.4} style={ { - "color": { - "dynamic": { - "dark": "rgba(255, 255, 255, 0.38)", - "light": "rgba(19, 19, 19, 0.35)", - }, - }, + "color": "rgba(19, 19, 19, 0.35)", "fontFamily": "Basel Grotesk", "fontSize": 19, "fontWeight": "400", @@ -413,12 +318,7 @@ exports[`renders text with few matches 1`] = ` maxFontSizeMultiplier={1.4} style={ { - "color": { - "dynamic": { - "dark": "rgba(255, 255, 255, 0.38)", - "light": "rgba(19, 19, 19, 0.35)", - }, - }, + "color": "rgba(19, 19, 19, 0.35)", "fontFamily": "Basel Grotesk", "fontSize": 19, "fontWeight": "400", @@ -434,12 +334,7 @@ exports[`renders text with few matches 1`] = ` maxFontSizeMultiplier={1.4} style={ { - "color": { - "dynamic": { - "dark": "rgba(255, 255, 255, 0.38)", - "light": "rgba(19, 19, 19, 0.35)", - }, - }, + "color": "rgba(19, 19, 19, 0.35)", "fontFamily": "Basel Grotesk", "fontSize": 19, "fontWeight": "400", @@ -455,12 +350,7 @@ exports[`renders text with few matches 1`] = ` maxFontSizeMultiplier={1.4} style={ { - "color": { - "dynamic": { - "dark": "rgba(255, 255, 255, 0.38)", - "light": "rgba(19, 19, 19, 0.35)", - }, - }, + "color": "rgba(19, 19, 19, 0.35)", "fontFamily": "Basel Grotesk", "fontSize": 19, "fontWeight": "400", @@ -481,12 +371,7 @@ exports[`renders text without matches 1`] = ` numberOfLines={1} style={ { - "color": { - "dynamic": { - "dark": "#FFFFFF", - "light": "#131313", - }, - }, + "color": "#131313", "fontFamily": "Basel Grotesk", "fontSize": 19, "fontWeight": "400", diff --git a/apps/mobile/src/features/biometrics/biometrics-utils.test.ts b/apps/mobile/src/features/biometrics/biometrics-utils.test.ts index 12959fa4481..a75f42ca282 100644 --- a/apps/mobile/src/features/biometrics/biometrics-utils.test.ts +++ b/apps/mobile/src/features/biometrics/biometrics-utils.test.ts @@ -19,7 +19,7 @@ describe(tryLocalAuthenticate, () => { it('checks enrollement', async () => { mockedHasHardwareAsync.mockResolvedValue(true) mockedIsEnrolledAsync.mockResolvedValue(false) - mockedAuthenticateAsync.mockResolvedValue({ success: false, error: '' }) + mockedAuthenticateAsync.mockResolvedValue({ success: false, error: 'unknown' }) const status = await tryLocalAuthenticate() @@ -29,7 +29,7 @@ describe(tryLocalAuthenticate, () => { it('fails to authenticate when user rejects', async () => { mockedHasHardwareAsync.mockResolvedValue(true) mockedIsEnrolledAsync.mockResolvedValue(true) - mockedAuthenticateAsync.mockResolvedValue({ success: false, error: '' }) + mockedAuthenticateAsync.mockResolvedValue({ success: false, error: 'unknown' }) const status = await tryLocalAuthenticate() diff --git a/apps/mobile/src/features/biometrics/biometricsSaga.ts b/apps/mobile/src/features/biometrics/biometricsSaga.ts index 9416bbaa5eb..2f36a784766 100644 --- a/apps/mobile/src/features/biometrics/biometricsSaga.ts +++ b/apps/mobile/src/features/biometrics/biometricsSaga.ts @@ -35,7 +35,7 @@ export function* biometricsSaga(): SagaIterator { // -------------------------------------------------------------------------------------------- const authTask: Task = yield* fork(function* watchAuthenticationTriggers(): SagaIterator { while (true) { - const action = yield* take(triggerAuthentication.type) + const action = yield* take(triggerAuthentication) yield* call(handleAuthentication, action) } }) diff --git a/apps/mobile/src/features/deepLinking/deepLinkUtils.test.ts b/apps/mobile/src/features/deepLinking/deepLinkUtils.test.ts index d1b329914e6..0cc51ce831d 100644 --- a/apps/mobile/src/features/deepLinking/deepLinkUtils.test.ts +++ b/apps/mobile/src/features/deepLinking/deepLinkUtils.test.ts @@ -1,10 +1,4 @@ import { DeepLinkAction, parseDeepLinkUrl } from 'src/features/deepLinking/deepLinkUtils' -import { logger } from 'utilities/src/logger/logger' - -// Mock the config utils -jest.mock('src/features/deepLinking/configUtils', () => ({ - getInAppBrowserAllowlist: jest.fn(() => ({ allowedUrls: [] })), -})) // Mock the logger jest.mock('utilities/src/logger/logger', () => ({ @@ -13,11 +7,6 @@ jest.mock('utilities/src/logger/logger', () => ({ }, })) -const mockGetInAppBrowserAllowlist = jest.mocked( - require('src/features/deepLinking/configUtils').getInAppBrowserAllowlist, -) -const mockLogger = jest.mocked(logger) - describe('getDeepLinkAction', () => { it.each` url | expected @@ -38,84 +27,11 @@ describe('getDeepLinkAction', () => { ${'uniswap://app/fiatonramp?userAddress=0x123&source=push'} | ${DeepLinkAction.FiatOnRampScreen} ${'uniswap://app/fiatonramp?source=push&moonpayOnly=true&moonpayCurrencyCode=usdc&amount=100'} | ${DeepLinkAction.FiatOnRampScreen} ${'uniswap://app/tokendetails?currencyId=10-0x6fd9d7ad17242c41f7131d257212c54a0e816691&source=push'} | ${DeepLinkAction.TokenDetails} + ${'https://cryptothegame.com/'} | ${DeepLinkAction.UniswapExternalBrowserLink} + ${'https://support.uniswap.org/hc/en-us/articles/test-article-123'} | ${DeepLinkAction.UniswapExternalBrowserLink} + ${'https://blog.uniswap.org/article'} | ${DeepLinkAction.UniswapExternalBrowserLink} + ${'https://uniswapx.uniswap.org/'} | ${DeepLinkAction.UniswapExternalBrowserLink} `('url=$url should return expected=$expected', ({ url, expected }) => { expect(parseDeepLinkUrl(url).action).toEqual(expected) }) }) - -describe('parseDeepLinkUrl allowlist behavior', () => { - beforeEach(() => { - jest.clearAllMocks() - }) - - describe('when allowlist is empty', () => { - beforeEach(() => { - mockGetInAppBrowserAllowlist.mockReturnValue({ allowedUrls: [] }) - }) - - it('should return Unknown action and log appropriate error message', () => { - const url = 'https://example.com/test' - const result = parseDeepLinkUrl(url) - - expect(result.action).toBe(DeepLinkAction.Unknown) - expect(mockLogger.error).toHaveBeenCalledWith( - `No allowlist configured for browser opening, rejecting URL: ${url}`, - { - tags: { file: 'deepLinkUtils', function: 'parseDeepLinkUrl' }, - }, - ) - expect(mockLogger.error).toHaveBeenCalledWith(`Unknown deep link action for url=${url}`, { - tags: { file: 'deepLinkUtils', function: 'parseDeepLinkUrl' }, - }) - }) - }) - - describe('when allowlist is non-empty but URL is not allowlisted', () => { - beforeEach(() => { - mockGetInAppBrowserAllowlist.mockReturnValue({ allowedUrls: ['https://trusted.com'] }) - }) - - it('should return Unknown action and log URL not allowlisted error', () => { - const url = 'https://untrusted.com/test' - const result = parseDeepLinkUrl(url) - - expect(result.action).toBe(DeepLinkAction.Unknown) - expect(mockLogger.error).toHaveBeenCalledWith(`URL not allowlisted for browser opening: ${url}`, { - tags: { file: 'deepLinkUtils', function: 'parseDeepLinkUrl' }, - }) - expect(mockLogger.error).toHaveBeenCalledWith(`Unknown deep link action for url=${url}`, { - tags: { file: 'deepLinkUtils', function: 'parseDeepLinkUrl' }, - }) - }) - }) - - describe('when URL is allowlisted', () => { - beforeEach(() => { - mockGetInAppBrowserAllowlist.mockReturnValue({ allowedUrls: [{ url: 'https://example.com', openInApp: true }] }) - }) - - it('should return InAppBrowser action without logging errors', () => { - const url = 'https://example.com/test' - const result = parseDeepLinkUrl(url) - - expect(result.action).toBe(DeepLinkAction.InAppBrowser) - if (result.action === DeepLinkAction.InAppBrowser) { - expect(result.data.targetUrl).toBe(url) - expect(result.data.openInApp).toBe(true) - } - expect(mockLogger.error).not.toHaveBeenCalled() - }) - - it('should respect openInApp configuration', () => { - mockGetInAppBrowserAllowlist.mockReturnValue({ allowedUrls: [{ url: 'https://example.com', openInApp: false }] }) - - const url = 'https://example.com/test' - const result = parseDeepLinkUrl(url) - - expect(result.action).toBe(DeepLinkAction.InAppBrowser) - if (result.action === DeepLinkAction.InAppBrowser) { - expect(result.data.openInApp).toBe(false) - } - }) - }) -}) diff --git a/apps/mobile/src/features/deepLinking/deepLinkUtils.ts b/apps/mobile/src/features/deepLinking/deepLinkUtils.ts index 318a9e14845..25082908907 100644 --- a/apps/mobile/src/features/deepLinking/deepLinkUtils.ts +++ b/apps/mobile/src/features/deepLinking/deepLinkUtils.ts @@ -1,7 +1,6 @@ import { DeepLinkUrlAllowlist } from '@universe/gating' import { getScantasticQueryParams } from 'src/components/Requests/ScanSheet/util' import { UNISWAP_URL_SCHEME_UWU_LINK } from 'src/components/Requests/Uwulink/utils' -import { getInAppBrowserAllowlist } from 'src/features/deepLinking/configUtils' import { UNISWAP_URL_SCHEME, UNISWAP_URL_SCHEME_SCANTASTIC, @@ -17,6 +16,7 @@ const WALLETCONNECT_URI_SCHEME = 'wc:' // https://eips.ethereum.org/EIPS/eip-132 export enum DeepLinkAction { UniswapWebLink = 'uniswapWebLink', + UniswapExternalBrowserLink = 'uniswapExternalBrowserLink', UniswapWalletConnect = 'uniswapWalletConnect', WalletConnectAsParam = 'walletConnectAsParam', UniswapWidget = 'uniswapWidget', @@ -29,7 +29,6 @@ export enum DeepLinkAction { SkipNonWalletConnect = 'skipNonWalletConnect', UniversalWalletConnectLink = 'universalWalletConnectLink', WalletConnect = 'walletConnect', - InAppBrowser = 'inAppBrowser', Error = 'error', Unknown = 'unknown', TokenDetails = 'tokenDetails', @@ -85,6 +84,7 @@ export type PayloadWithFiatOnRampParams = BasePayload & { export type DeepLinkActionResult = | { action: DeepLinkAction.UniswapWebLink; data: BasePayload & { urlPath: string } } + | { action: DeepLinkAction.UniswapExternalBrowserLink; data: BasePayload & { urlPath: string } } | { action: DeepLinkAction.WalletConnectAsParam; data: PayloadWithWcUri } | { action: DeepLinkAction.UniswapWalletConnect; data: PayloadWithWcUri } | { action: DeepLinkAction.UniswapWidget; data: BasePayload } @@ -97,7 +97,6 @@ export type DeepLinkActionResult = | { action: DeepLinkAction.SkipNonWalletConnect; data: BasePayload } | { action: DeepLinkAction.UniversalWalletConnectLink; data: PayloadWithWcUri } | { action: DeepLinkAction.WalletConnect; data: BasePayload & { wcUri: string } } - | { action: DeepLinkAction.InAppBrowser; data: BasePayload & { targetUrl: string; openInApp: boolean } } | { action: DeepLinkAction.TokenDetails; data: BasePayload & { currencyId: string } } | { action: DeepLinkAction.FiatOnRampScreen; data: PayloadWithFiatOnRampParams } | { action: DeepLinkAction.Error; data: BasePayload } @@ -105,56 +104,6 @@ export type DeepLinkActionResult = type DeepLinkHandler = (url: URL, data: BasePayload) => DeepLinkActionResult -/** - * Checks if a URL is allowlisted for browser opening and returns the configuration. - * This function should be called with the dynamic config value. - * - * @param urlString - The URL to check. - * @param allowList - Allowlist from dynamic config. - * @returns Object with isAllowed and openInApp flags, or null if not allowlisted. - */ -function getUrlAllowlistConfig( - urlString: string, - allowList: DeepLinkUrlAllowlist, -): { isAllowed: boolean; openInApp: boolean } { - try { - const url = new URL(urlString) - - // Only allow HTTPS protocol - if (url.protocol !== 'https:') { - return { isAllowed: false, openInApp: false } - } - - const urlToCheck = `${url.protocol}//${url.hostname}${url.pathname}` - - for (const allowedItem of allowList.allowedUrls) { - const allowedUrl = typeof allowedItem === 'string' ? allowedItem : allowedItem.url - const openInApp = typeof allowedItem === 'string' ? true : (allowedItem.openInApp ?? true) // Default to in-app - - try { - // Support both exact matches and hostname matches - if (allowedUrl === urlString || allowedUrl === urlToCheck) { - return { isAllowed: true, openInApp } - } - - // Support hostname-only matches (e.g., "example.com" matches "https://example.com/any/path") - // Always use HTTPS for allowed URL validation - const allowedUrlObj = new URL(allowedUrl.startsWith('https://') ? allowedUrl : `https://${allowedUrl}`) - if (url.hostname === allowedUrlObj.hostname) { - return { isAllowed: true, openInApp } - } - } catch { - // If allowedUrl is not a valid URL, reject it for security - continue - } - } - - return { isAllowed: false, openInApp: false } - } catch { - return { isAllowed: false, openInApp: false } - } -} - /** * Parses a deep link URL and returns the action to be taken as well as * any additional data that may be needed to handle the deep link. @@ -174,6 +123,10 @@ export function parseDeepLinkUrl(urlString: string): DeepLinkActionResult { } } + if (isValidUniswapExternalWebLink(urlString)) { + return { action: DeepLinkAction.UniswapExternalBrowserLink, data: { ...data, urlPath: url.pathname } } + } + const urlPath = url.pathname const userAddress = url.searchParams.get('userAddress') ?? undefined const fiatOnRamp = url.searchParams.get('fiatOnRamp') === 'true' @@ -290,35 +243,8 @@ export function parseDeepLinkUrl(urlString: string): DeepLinkActionResult { return { action: DeepLinkAction.WalletConnect, data: { ...data, wcUri } } } - // Check if URL is allowlisted for browser opening - const inAppBrowserAllowlist = getInAppBrowserAllowlist() - - // Always perform allowlist check for consistent behavior and logging - const allowlistConfig = getUrlAllowlistConfig(urlString, inAppBrowserAllowlist) - if (allowlistConfig.isAllowed) { - return { - action: DeepLinkAction.InAppBrowser, - data: { ...data, targetUrl: urlString, openInApp: allowlistConfig.openInApp }, - } - } - - // Log appropriate message based on allowlist state - if (inAppBrowserAllowlist.allowedUrls.length === 0) { - logger.error(`No allowlist configured for browser opening, rejecting URL: ${urlString}`, { - tags: { file: 'deepLinkUtils', function: 'parseDeepLinkUrl' }, - }) - } else { - logger.error(`URL not allowlisted for browser opening: ${urlString}`, { - tags: { file: 'deepLinkUtils', function: 'parseDeepLinkUrl' }, - }) - } - - logger.error(`Unknown deep link action for url=${urlString}`, { - tags: { file: 'deepLinkUtils', function: 'parseDeepLinkUrl' }, - }) return { action: DeepLinkAction.Unknown, data } } - const handlers: Record = { [UNISWAP_WEB_HOSTNAME]: (url, data) => { const urlParts = url.href.split(`${UNISWAP_WEB_HOSTNAME}/`) @@ -368,6 +294,17 @@ const handlers: Record = { }), } +const UNISWAP_EXTERNAL_WEB_LINK_VALID_REGEXES = [ + // eslint-disable-next-line security/detect-unsafe-regex + /^https:\/\/([a-zA-Z0-9-]+)\.uniswap\.org(\/.*)?$/, + // eslint-disable-next-line security/detect-unsafe-regex + /^https:\/\/cryptothegame\.com(\/.*)?$/, +] + +function isValidUniswapExternalWebLink(urlString: string): boolean { + return UNISWAP_EXTERNAL_WEB_LINK_VALID_REGEXES.some((regex) => regex.test(urlString)) +} + /** * Extracts the WalletConnect URI from a URL string. * diff --git a/apps/mobile/src/features/deepLinking/handleDeepLinkSaga.test.ts b/apps/mobile/src/features/deepLinking/handleDeepLinkSaga.test.ts index 9424abf457a..f3133c385fd 100644 --- a/apps/mobile/src/features/deepLinking/handleDeepLinkSaga.test.ts +++ b/apps/mobile/src/features/deepLinking/handleDeepLinkSaga.test.ts @@ -10,7 +10,6 @@ import { ONRAMP_DEEPLINK_DELAY, parseAndValidateUserAddress, } from 'src/features/deepLinking/handleDeepLinkSaga' -import { handleInAppBrowser } from 'src/features/deepLinking/handleInAppBrowserSaga' import { handleOnRampReturnLink } from 'src/features/deepLinking/handleOnRampReturnLinkSaga' import { handleTransactionLink } from 'src/features/deepLinking/handleTransactionLinkSaga' import { handleUniswapAppDeepLink } from 'src/features/deepLinking/handleUniswapAppDeepLink' @@ -50,16 +49,6 @@ jest.mock('@universe/gating', () => ({ getFeatureFlag: jest.fn(() => false), // Default to false for feature flags })) -jest.mock('src/features/deepLinking/configUtils', () => ({ - getInAppBrowserAllowlist: jest.fn(() => ({ allowedUrls: [] })), // Default to empty allowlist - getUwuLinkAllowlist: jest.fn(() => ({ contracts: [], tokenRecipients: [] })), // Default to empty allowlist -})) - -// Get the mocked functions for proper typing -const mockGetInAppBrowserAllowlist = jest.mocked( - require('src/features/deepLinking/configUtils').getInAppBrowserAllowlist, -) - const account = signerMnemonicAccount() const swapUrl = `https://uniswap.org/app?screen=swap&userAddress=${account.address}&inputCurrencyId=${SAMPLE_CURRENCY_ID_1}&outputCurrencyId=${SAMPLE_CURRENCY_ID_2}¤cyField=INPUT` @@ -658,363 +647,4 @@ describe(handleDeepLink, () => { .returns(undefined) .silentRun() }) - - describe('In-app browser functionality', () => { - const testUrl = 'https://example.com/test' - const testUrlPayload = { url: testUrl, coldStart: false } - - beforeEach(() => { - // Reset the mock before each test - mockGetInAppBrowserAllowlist.mockClear() - }) - - it('Handles allowlisted URL with openInApp=true (default)', () => { - const mockAllowlist = [{ url: 'https://example.com', openInApp: true }] - mockGetInAppBrowserAllowlist.mockReturnValue({ allowedUrls: mockAllowlist }) - - return expectSaga(handleDeepLink, { payload: testUrlPayload, type: '' }) - .withState(stateWithActiveAccountAddress) - .call(handleInAppBrowser, testUrl, true) - .call(sendAnalyticsEvent, MobileEventName.DeepLinkOpened, { - action: DeepLinkAction.InAppBrowser, - url: testUrl, - screen: 'other', - is_cold_start: false, - source: 'unknown', - }) - .returns(undefined) - .silentRun() - }) - - it('Handles allowlisted URL with openInApp=false (external browser)', () => { - const mockAllowlist = [{ url: 'https://example.com', openInApp: false }] - mockGetInAppBrowserAllowlist.mockReturnValue({ allowedUrls: mockAllowlist }) - - return expectSaga(handleDeepLink, { payload: testUrlPayload, type: '' }) - .withState(stateWithActiveAccountAddress) - .call(handleInAppBrowser, testUrl, false) - .call(sendAnalyticsEvent, MobileEventName.DeepLinkOpened, { - action: DeepLinkAction.InAppBrowser, - url: testUrl, - screen: 'other', - is_cold_start: false, - source: 'unknown', - }) - .returns(undefined) - .silentRun() - }) - - it('Handles allowlisted URL with string format (defaults to openInApp=true)', () => { - const mockAllowlist = ['https://example.com'] - mockGetInAppBrowserAllowlist.mockReturnValue({ allowedUrls: mockAllowlist }) - - return expectSaga(handleDeepLink, { payload: testUrlPayload, type: '' }) - .withState(stateWithActiveAccountAddress) - .call(handleInAppBrowser, testUrl, true) - .call(sendAnalyticsEvent, MobileEventName.DeepLinkOpened, { - action: DeepLinkAction.InAppBrowser, - url: testUrl, - screen: 'other', - is_cold_start: false, - source: 'unknown', - }) - .returns(undefined) - .silentRun() - }) - - it('Handles hostname matching with openInApp configuration', () => { - const mockAllowlist = [{ url: 'example.com', openInApp: false }] - mockGetInAppBrowserAllowlist.mockReturnValue({ allowedUrls: mockAllowlist }) - - return expectSaga(handleDeepLink, { payload: testUrlPayload, type: '' }) - .withState(stateWithActiveAccountAddress) - .call(handleInAppBrowser, testUrl, false) - .call(sendAnalyticsEvent, MobileEventName.DeepLinkOpened, { - action: DeepLinkAction.InAppBrowser, - url: testUrl, - screen: 'other', - is_cold_start: false, - source: 'unknown', - }) - .returns(undefined) - .silentRun() - }) - - it('Handles allowlisted URL without active account', () => { - const mockAllowlist = [{ url: 'https://example.com', openInApp: true }] - mockGetInAppBrowserAllowlist.mockReturnValue({ allowedUrls: mockAllowlist }) - - return expectSaga(handleDeepLink, { payload: testUrlPayload, type: '' }) - .withState({ - wallet: { - accounts: {}, - activeAccountAddress: null, - }, - }) - .call(handleInAppBrowser, testUrl, true) - .call(sendAnalyticsEvent, MobileEventName.DeepLinkOpened, { - action: DeepLinkAction.InAppBrowser, - url: testUrl, - screen: 'other', - is_cold_start: false, - source: 'unknown', - }) - .returns(undefined) - .silentRun() - }) - - it('Rejects non-allowlisted URL and logs error', () => { - const mockAllowlist = [{ url: 'https://trusted.com', openInApp: true }] - mockGetInAppBrowserAllowlist.mockReturnValue({ allowedUrls: mockAllowlist }) - - const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined) - - return expectSaga(handleDeepLink, { payload: testUrlPayload, type: '' }) - .withState(stateWithActiveAccountAddress) - .call(sendAnalyticsEvent, MobileEventName.DeepLinkOpened, { - action: DeepLinkAction.Unknown, - url: testUrl, - screen: 'other', - is_cold_start: false, - source: 'unknown', - }) - .returns(undefined) - .silentRun() - .finally(() => { - consoleSpy.mockRestore() - }) - }) - - it('Handles mixed allowlist with different URL formats and openInApp settings', () => { - const testUrl2 = 'https://docs.example.com/help' - const testUrl2Payload = { url: testUrl2, coldStart: false } - - const mockAllowlist = [ - 'https://example.com', // String format - defaults to openInApp=true - { url: 'https://docs.example.com', openInApp: false }, // Object format - external browser - { url: 'trusted-site.com', openInApp: true }, // Hostname matching - in-app browser - ] - mockGetInAppBrowserAllowlist.mockReturnValue({ allowedUrls: mockAllowlist }) - - return expectSaga(handleDeepLink, { payload: testUrl2Payload, type: '' }) - .withState(stateWithActiveAccountAddress) - .call(handleInAppBrowser, testUrl2, false) - .call(sendAnalyticsEvent, MobileEventName.DeepLinkOpened, { - action: DeepLinkAction.InAppBrowser, - url: testUrl2, - screen: 'other', - is_cold_start: false, - source: 'unknown', - }) - .returns(undefined) - .silentRun() - }) - - it('Handles empty allowlist', () => { - mockGetInAppBrowserAllowlist.mockReturnValue({ allowedUrls: [] }) - - const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined) - - return expectSaga(handleDeepLink, { payload: testUrlPayload, type: '' }) - .withState(stateWithActiveAccountAddress) - .call(sendAnalyticsEvent, MobileEventName.DeepLinkOpened, { - action: DeepLinkAction.Unknown, - url: testUrl, - screen: 'other', - is_cold_start: false, - source: 'unknown', - }) - .returns(undefined) - .silentRun() - .finally(() => { - consoleSpy.mockRestore() - }) - }) - - it('Handles URL with query parameters and fragments', () => { - const complexUrl = 'https://example.com/path?param=value#section' - const complexUrlPayload = { url: complexUrl, coldStart: false } - - const mockAllowlist = [{ url: 'https://example.com', openInApp: true }] - mockGetInAppBrowserAllowlist.mockReturnValue({ allowedUrls: mockAllowlist }) - - return expectSaga(handleDeepLink, { payload: complexUrlPayload, type: '' }) - .withState(stateWithActiveAccountAddress) - .call(handleInAppBrowser, complexUrl, true) - .call(sendAnalyticsEvent, MobileEventName.DeepLinkOpened, { - action: DeepLinkAction.InAppBrowser, - url: complexUrl, - screen: 'other', - is_cold_start: false, - source: 'unknown', - }) - .returns(undefined) - .silentRun() - }) - - // Security tests for URL validation - describe('URL validation security', () => { - const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined) - - afterEach(() => { - consoleSpy.mockClear() - }) - - afterAll(() => { - consoleSpy.mockRestore() - }) - - it('Rejects malicious URLs that would match allowlist substrings', () => { - // Test case: allowlist contains "example.com" but malicious URL contains it as substring - const mockAllowlist = ['example.com'] - mockGetInAppBrowserAllowlist.mockReturnValue({ allowedUrls: mockAllowlist }) - - const maliciousUrls = [ - 'https://malicious-example.com/steal-data', - 'https://notexample.com/example.com', - 'https://example.com.evil.com/phishing', - 'https://sub.example.com.attacker.com/fake', - ] - - const testMaliciousUrl = (maliciousUrl: string): Promise => { - const payload = { url: maliciousUrl, coldStart: false } - return expectSaga(handleDeepLink, { payload, type: '' }) - .withState(stateWithActiveAccountAddress) - .call(sendAnalyticsEvent, MobileEventName.DeepLinkOpened, { - action: DeepLinkAction.Unknown, - url: maliciousUrl, - screen: 'other', - is_cold_start: false, - source: 'unknown', - }) - .returns(undefined) - .silentRun() - } - - const promises = maliciousUrls.map(testMaliciousUrl) - return Promise.all(promises) - }) - - it('Rejects URLs with invalid allowlist entries that would previously use includes() fallback', () => { - // Test case: allowlist contains invalid URL strings that would trigger the dangerous fallback - const mockAllowlist = [ - 'invalid-url-format', // This is not a valid URL - '://malformed-url', - 'just-a-string', - ] - mockGetInAppBrowserAllowlist.mockReturnValue({ allowedUrls: mockAllowlist }) - - const invalidTestUrls = [ - 'https://malicious-invalid-url-format.com/attack', - 'https://evil.com/invalid-url-format', - 'https://attacker.com/path?param=invalid-url-format', - ] - - const testInvalidUrl = (invalidTestUrl: string): Promise => { - const payload = { url: invalidTestUrl, coldStart: false } - return expectSaga(handleDeepLink, { payload, type: '' }) - .withState(stateWithActiveAccountAddress) - .call(sendAnalyticsEvent, MobileEventName.DeepLinkOpened, { - action: DeepLinkAction.Unknown, - url: invalidTestUrl, - screen: 'other', - is_cold_start: false, - source: 'unknown', - }) - .returns(undefined) - .silentRun() - } - - const promises = invalidTestUrls.map(testInvalidUrl) - return Promise.all(promises) - }) - - it('Still allows legitimate URLs that match hostname exactly', () => { - const mockAllowlist = ['example.com'] - mockGetInAppBrowserAllowlist.mockReturnValue({ allowedUrls: mockAllowlist }) - - const legitimateUrls = [ - 'https://example.com/', - 'https://example.com/path', - 'https://example.com/path?param=value', - 'https://example.com/path?param=value#fragment', - ] - - const testLegitimateUrl = (legitimateUrl: string): Promise => { - const payload = { url: legitimateUrl, coldStart: false } - return expectSaga(handleDeepLink, { payload, type: '' }) - .withState(stateWithActiveAccountAddress) - .call(handleInAppBrowser, legitimateUrl, true) - .call(sendAnalyticsEvent, MobileEventName.DeepLinkOpened, { - action: DeepLinkAction.InAppBrowser, - url: legitimateUrl, - screen: 'other', - is_cold_start: false, - source: 'unknown', - }) - .returns(undefined) - .silentRun() - } - - const promises = legitimateUrls.map(testLegitimateUrl) - return Promise.all(promises) - }) - - it('Rejects non-HTTPS URLs even if they match allowlist', () => { - const mockAllowlist = ['example.com'] - mockGetInAppBrowserAllowlist.mockReturnValue({ allowedUrls: mockAllowlist }) - - const insecureUrls = ['http://example.com/', 'ftp://example.com/', 'file://example.com/'] - - const testInsecureUrl = (insecureUrl: string): Promise => { - const payload = { url: insecureUrl, coldStart: false } - return expectSaga(handleDeepLink, { payload, type: '' }) - .withState(stateWithActiveAccountAddress) - .call(sendAnalyticsEvent, MobileEventName.DeepLinkOpened, { - action: DeepLinkAction.Unknown, - url: insecureUrl, - screen: 'other', - is_cold_start: false, - source: 'unknown', - }) - .returns(undefined) - .silentRun() - } - - const promises = insecureUrls.map(testInsecureUrl) - return Promise.all(promises) - }) - - it('Handles edge cases with subdomain attacks', () => { - const mockAllowlist = ['trusted.com'] - mockGetInAppBrowserAllowlist.mockReturnValue({ allowedUrls: mockAllowlist }) - - // These should be rejected - they are different hostnames - const subdomainAttackUrls = [ - 'https://evil.trusted.com.attacker.com/', - 'https://trusted.com.evil.com/', - 'https://anytrusted.com/', - 'https://trusted.com.fake/', - ] - - const testAttackUrl = (attackUrl: string): Promise => { - const payload = { url: attackUrl, coldStart: false } - return expectSaga(handleDeepLink, { payload, type: '' }) - .withState(stateWithActiveAccountAddress) - .call(sendAnalyticsEvent, MobileEventName.DeepLinkOpened, { - action: DeepLinkAction.Unknown, - url: attackUrl, - screen: 'other', - is_cold_start: false, - source: 'unknown', - }) - .returns(undefined) - .silentRun() - } - - const promises = subdomainAttackUrls.map(testAttackUrl) - return Promise.all(promises) - }) - }) - }) }) diff --git a/apps/mobile/src/features/deepLinking/handleDeepLinkSaga.ts b/apps/mobile/src/features/deepLinking/handleDeepLinkSaga.ts index c115585ae5b..ffa647d65d9 100644 --- a/apps/mobile/src/features/deepLinking/handleDeepLinkSaga.ts +++ b/apps/mobile/src/features/deepLinking/handleDeepLinkSaga.ts @@ -16,7 +16,6 @@ import { PayloadWithFiatOnRampParams, parseDeepLinkUrl, } from 'src/features/deepLinking/deepLinkUtils' -import { handleInAppBrowser } from 'src/features/deepLinking/handleInAppBrowserSaga' import { handleOffRampReturnLink } from 'src/features/deepLinking/handleOffRampReturnLinkSaga' import { handleOnRampReturnLink } from 'src/features/deepLinking/handleOnRampReturnLinkSaga' import { handleSwapLink } from 'src/features/deepLinking/handleSwapLinkSaga' @@ -66,8 +65,6 @@ export function* handleDeepLink(action: ReturnType) { if (!activeAccount) { if (deepLinkAction.action === DeepLinkAction.UniswapWebLink) { yield* call(openUri, { uri: deepLinkAction.data.url.toString(), openExternalBrowser: true }) - } else if (deepLinkAction.action === DeepLinkAction.InAppBrowser) { - yield* call(handleInAppBrowser, deepLinkAction.data.targetUrl, deepLinkAction.data.openInApp) } // If there is no active account, we don't want to handle other deep links } else { @@ -80,6 +77,10 @@ export function* handleDeepLink(action: ReturnType) { }) break } + case DeepLinkAction.UniswapExternalBrowserLink: { + yield* call(openUri, { uri: deepLinkAction.data.url.toString(), openExternalBrowser: true }) + break + } case DeepLinkAction.WalletConnectAsParam: case DeepLinkAction.UniswapWalletConnect: { yield* call(handleWalletConnectDeepLink, deepLinkAction.data.wcUri) @@ -101,10 +102,6 @@ export function* handleDeepLink(action: ReturnType) { yield* call(handleUwuLinkDeepLink, deepLinkAction.data.url.toString()) break } - case DeepLinkAction.InAppBrowser: { - yield* call(handleInAppBrowser, deepLinkAction.data.targetUrl, deepLinkAction.data.openInApp) - break - } case DeepLinkAction.TransactionScreen: case DeepLinkAction.ShowTransactionAfterFiatOnRamp: case DeepLinkAction.ShowTransactionAfterFiatOffRampScreen: diff --git a/apps/mobile/src/features/deepLinking/handleInAppBrowserSaga.ts b/apps/mobile/src/features/deepLinking/handleInAppBrowserSaga.ts deleted file mode 100644 index 3503cfc20f4..00000000000 --- a/apps/mobile/src/features/deepLinking/handleInAppBrowserSaga.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { call } from 'typed-redux-saga' -import { openUri } from 'uniswap/src/utils/linking' -import { logger } from 'utilities/src/logger/logger' - -/** - * Opens a URL in a browser window (in-app or external). - * - * @param url - The URL to open - * @param openInApp - If true, opens in in-app browser; if false, opens in external browser - */ -export function* handleInAppBrowser(url: string, openInApp: boolean = true) { - try { - const browserType = openInApp ? 'in-app browser' : 'external browser' - yield* call(logger.info, 'handleInAppBrowserSaga', 'handleInAppBrowser', `Opening URL in ${browserType}: ${url}`) - - // Open the URL using openUri with the specified browser preference - yield* call(openUri, { - uri: url, - openExternalBrowser: !openInApp, // Use external browser if openInApp is false - isSafeUri: true, // URL has been allowlisted so it's safe - }) - - yield* call( - logger.info, - 'handleInAppBrowserSaga', - 'handleInAppBrowser', - `Successfully opened URL in ${browserType}: ${url}`, - ) - } catch (error) { - yield* call(logger.error, error, { - tags: { file: 'handleInAppBrowserSaga', function: 'handleInAppBrowser' }, - extra: { url }, - }) - } -} diff --git a/apps/mobile/src/features/deepLinking/parseSwapLink.test.ts b/apps/mobile/src/features/deepLinking/parseSwapLink.test.ts index c6a97acd5ba..921fe494337 100644 --- a/apps/mobile/src/features/deepLinking/parseSwapLink.test.ts +++ b/apps/mobile/src/features/deepLinking/parseSwapLink.test.ts @@ -186,19 +186,6 @@ describe('parseSwapLink', () => { expect(result.exactAmountToken).toBe('1.5') }) - it('should parse valid MonadTestnet link', () => { - const url = new URL( - `https://uniswap.org/mobile-redirect?screen=swap&inputCurrencyId=${UniverseChainId.MonadTestnet}-0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee&outputCurrencyId=${UniverseChainId.MonadTestnet}-0x1234567890123456789012345678901234567890¤cyField=output&amount=100`, - ) - - const result = parseSwapLinkMobileFormatOrThrow(url) - - expect(result.inputAsset?.chainId).toBe(UniverseChainId.MonadTestnet) - expect(result.outputAsset?.chainId).toBe(UniverseChainId.MonadTestnet) - expect(result.exactCurrencyField).toBe(CurrencyField.OUTPUT) - expect(result.exactAmountToken).toBe('100') - }) - it('should parse valid UnichainSepolia link', () => { const url = new URL( `https://uniswap.org/mobile-redirect?screen=swap&inputCurrencyId=${UniverseChainId.UnichainSepolia}-0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee&outputCurrencyId=${UniverseChainId.UnichainSepolia}-0x31d0220469e10c4E71834a79b1f276d740d3768F¤cyField=input&amount=0.5`, @@ -325,14 +312,14 @@ describe('parseSwapLink', () => { expect(result.outputAsset?.chainId).toBe(UniverseChainId.UnichainSepolia) }) - it('should allow swaps between MonadTestnet and Sepolia', () => { + it('should allow swaps between UnichainSepolia and Sepolia', () => { const url = new URL( - `https://uniswap.org/mobile-redirect?screen=swap&inputCurrencyId=${UniverseChainId.MonadTestnet}-0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee&outputCurrencyId=${UniverseChainId.Sepolia}-0x1c7d4b196cb0c7b01d743fbc6116a902379c7238¤cyField=output&amount=50`, + `https://uniswap.org/mobile-redirect?screen=swap&inputCurrencyId=${UniverseChainId.UnichainSepolia}-0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee&outputCurrencyId=${UniverseChainId.Sepolia}-0x1c7d4b196cb0c7b01d743fbc6116a902379c7238¤cyField=output&amount=50`, ) const result = parseSwapLinkMobileFormatOrThrow(url) - expect(result.inputAsset?.chainId).toBe(UniverseChainId.MonadTestnet) + expect(result.inputAsset?.chainId).toBe(UniverseChainId.UnichainSepolia) expect(result.outputAsset?.chainId).toBe(UniverseChainId.Sepolia) expect(result.exactCurrencyField).toBe(CurrencyField.OUTPUT) }) diff --git a/apps/mobile/src/features/externalProfile/ProfileContextMenu.tsx b/apps/mobile/src/features/externalProfile/ProfileContextMenu.tsx index e81e599aec4..05edb8d131f 100644 --- a/apps/mobile/src/features/externalProfile/ProfileContextMenu.tsx +++ b/apps/mobile/src/features/externalProfile/ProfileContextMenu.tsx @@ -4,9 +4,8 @@ import { useTranslation } from 'react-i18next' import { NativeSyntheticEvent, Share } from 'react-native' import ContextMenu, { ContextMenuOnPressNativeEvent } from 'react-native-context-menu-view' import { useDispatch } from 'react-redux' -import { TripleDot } from 'src/components/icons/TripleDot' -import { Flex, TouchableArea } from 'ui/src' -import { iconSizes } from 'ui/src/theme' +import { TouchableArea } from 'ui/src' +import { Ellipsis } from 'ui/src/components/icons' import { uniswapUrls } from 'uniswap/src/constants/urls' import { useUnitagsAddressQuery } from 'uniswap/src/data/apiClients/unitagsApi/useUnitagsAddressQuery' import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' @@ -121,10 +120,8 @@ export function ProfileContextMenu({ address }: { address: Address }): JSX.Eleme await menuActions[e.nativeEvent.index]?.action() }} > - - - - + + ) diff --git a/apps/mobile/src/features/externalProfile/ProfileHeader.tsx b/apps/mobile/src/features/externalProfile/ProfileHeader.tsx index 3977d01811a..b20e8a63c14 100644 --- a/apps/mobile/src/features/externalProfile/ProfileHeader.tsx +++ b/apps/mobile/src/features/externalProfile/ProfileHeader.tsx @@ -156,7 +156,7 @@ export const ProfileHeader = memo(function ProfileHeader({ address }: ProfileHea {/* header row */} - + diff --git a/apps/mobile/src/features/fiatOnRamp/FiatOnRampAmountSection.tsx b/apps/mobile/src/features/fiatOnRamp/FiatOnRampAmountSection.tsx index d2915126388..8934eb2d257 100644 --- a/apps/mobile/src/features/fiatOnRamp/FiatOnRampAmountSection.tsx +++ b/apps/mobile/src/features/fiatOnRamp/FiatOnRampAmountSection.tsx @@ -75,7 +75,7 @@ interface FiatOnRampAmountSectionProps { } export type FiatOnRampAmountSectionRef = { - textInputRef: RefObject + textInputRef: RefObject triggerShakeAnimation: () => void } diff --git a/apps/mobile/src/features/import/InputWIthSuffixProps.ts b/apps/mobile/src/features/import/InputWIthSuffixProps.ts index b78f87f62f9..bf24dd3d029 100644 --- a/apps/mobile/src/features/import/InputWIthSuffixProps.ts +++ b/apps/mobile/src/features/import/InputWIthSuffixProps.ts @@ -14,7 +14,7 @@ export interface InputWithSuffixProps { multiline?: boolean textAlign?: 'left' | 'right' | 'center' lineHeight?: number - textInputRef: React.RefObject + textInputRef: React.RefObject onBlur?: () => void onFocus?: () => void onChangeText?: (text: string) => void diff --git a/apps/mobile/src/features/import/__snapshots__/GenericImportForm.test.tsx.snap b/apps/mobile/src/features/import/__snapshots__/GenericImportForm.test.tsx.snap index 8e1ea06dfc3..285b81bdcad 100644 --- a/apps/mobile/src/features/import/__snapshots__/GenericImportForm.test.tsx.snap +++ b/apps/mobile/src/features/import/__snapshots__/GenericImportForm.test.tsx.snap @@ -14,42 +14,17 @@ exports[`GenericImportForm renders a placeholder when there is no value 1`] = ` - - + + diff --git a/apps/mobile/src/features/nfts/item/__snapshots__/CollectionPreviewCard.test.tsx.snap b/apps/mobile/src/features/nfts/item/__snapshots__/CollectionPreviewCard.test.tsx.snap index 5bbef99eb00..ab767ca37f6 100644 --- a/apps/mobile/src/features/nfts/item/__snapshots__/CollectionPreviewCard.test.tsx.snap +++ b/apps/mobile/src/features/nfts/item/__snapshots__/CollectionPreviewCard.test.tsx.snap @@ -5,11 +5,34 @@ exports[`renders collection preview card 1`] = ` collapsable={false} focusVisibleStyle={{}} forwardedRef={[Function]} + jestAnimatedProps={ + { + "value": {}, + } + } jestAnimatedStyle={ { "value": {}, } } + jestInlineStyle={ + [ + { + "backgroundColor": "transparent", + "borderBottomLeftRadius": 12, + "borderBottomRightRadius": 12, + "borderTopLeftRadius": 12, + "borderTopRightRadius": 12, + "flexDirection": "column", + "opacity": 1, + "transform": [ + { + "scale": 1, + }, + ], + }, + ] + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -22,25 +45,26 @@ exports[`renders collection preview card 1`] = ` onStartShouldSetResponder={[Function]} role="button" style={ - { - "backgroundColor": "transparent", - "borderBottomLeftRadius": 12, - "borderBottomRightRadius": 12, - "borderTopLeftRadius": 12, - "borderTopRightRadius": 12, - "flexDirection": "column", - "opacity": 1, - "transform": [ - { - "scale": 1, - }, - ], - } + [ + { + "backgroundColor": "transparent", + "borderBottomLeftRadius": 12, + "borderBottomRightRadius": 12, + "borderTopLeftRadius": 12, + "borderTopRightRadius": 12, + "flexDirection": "column", + "opacity": 1, + "transform": [ + { + "scale": 1, + }, + ], + }, + {}, + ] } > { OneSignal.initialize(config.onesignalAppId) + startSilentPushListener() + OneSignal.Notifications.addEventListener('foregroundWillDisplay', (event) => { const notification = event.getNotification() const additionalData = notification.additionalData as { notification_type?: string } | undefined @@ -72,6 +75,12 @@ export const initOneSignal = (): void => { export const promptPushPermission = async (): Promise => { const response = await OneSignal.Notifications.requestPermission(true) logger.debug('Onesignal', 'promptForPushNotificationsWithUserResponse', `Prompt response: ${response}`) + + // Explicitly opt in to push notifications if permission was granted + if (response) { + OneSignal.User.pushSubscription.optIn() + } + return response } diff --git a/apps/mobile/src/features/notifications/SilentPushListener.ts b/apps/mobile/src/features/notifications/SilentPushListener.ts new file mode 100644 index 00000000000..395825bd05e --- /dev/null +++ b/apps/mobile/src/features/notifications/SilentPushListener.ts @@ -0,0 +1,70 @@ +import { NativeEventEmitter, NativeModules, Platform } from 'react-native' +import { WalletEventName } from 'uniswap/src/features/telemetry/constants/wallet' +import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' +import { logger } from 'utilities/src/logger/logger' +import { isMobileApp } from 'utilities/src/platform' + +const EVENT_NAME = 'SilentPushReceived' + +interface SilentPushEventEmitterInterface { + addListener: (eventName: string) => void + removeListeners: (count: number) => void +} + +declare module 'react-native' { + interface NativeModulesStatic { + SilentPushEventEmitter: SilentPushEventEmitterInterface + } +} + +const { SilentPushEventEmitter } = NativeModules + +const eventEmitter = isMobileApp ? new NativeEventEmitter(SilentPushEventEmitter) : undefined + +let subscription: { remove: () => void } | undefined + +const handleSilentPush = (payload: Record): void => { + logger.debug('SilentPush', 'handleSilentPush', 'Silent push received', payload) + + // Onesignal Silent Push payload stores the template id in the 'custom' object with key 'i' + if ('custom' in payload && payload.custom) { + try { + const customPayload = typeof payload.custom === 'string' ? JSON.parse(payload.custom) : payload.custom + + if (!('i' in customPayload) || typeof customPayload.i !== 'string') { + return + } + + sendAnalyticsEvent(WalletEventName.SilentPushReceived, { + template_id: customPayload.i, + }) + logger.debug('SilentPush', 'handleSilentPush', 'Silent push event sent', { + template_id: customPayload.i, + }) + } catch (error) { + logger.error(error, { + tags: { + file: 'SilentPushListener.ts', + function: 'handleSilentPush', + }, + }) + } + } +} + +export const startSilentPushListener = (): void => { + if (subscription) { + return + } + + if (!eventEmitter) { + logger.warn('SilentPush', 'startSilentPushListener', 'Native event emitter unavailable', { + platform: Platform.OS, + moduleLoaded: Boolean(SilentPushEventEmitter), + }) + return + } + + subscription = eventEmitter.addListener(EVENT_NAME, handleSilentPush) + logger.debug('SilentPush', 'startSilentPushListener', 'Listener registered') +} diff --git a/apps/mobile/src/features/notifications/constants.ts b/apps/mobile/src/features/notifications/constants.ts index ca53da9644f..00726dd0e13 100644 --- a/apps/mobile/src/features/notifications/constants.ts +++ b/apps/mobile/src/features/notifications/constants.ts @@ -11,6 +11,7 @@ export enum OneSignalUserTagField { SwapLastCompletedAt = 'swap_last_completed_at', AccountIsUnfunded = 'account_is_unfunded', GatingUnfundedWalletsEnabled = 'gating_unfunded_wallets_enabled', + ActiveWalletAddress = 'active_wallet_address', } export enum NotificationType { diff --git a/apps/mobile/src/features/notifications/saga.ts b/apps/mobile/src/features/notifications/saga.ts index 2f25dd8ed5e..edef583fd6c 100644 --- a/apps/mobile/src/features/notifications/saga.ts +++ b/apps/mobile/src/features/notifications/saga.ts @@ -6,14 +6,18 @@ import { call, select, takeEvery } from 'typed-redux-saga' import { finalizeTransaction } from 'uniswap/src/features/transactions/slice' import { TransactionStatus, TransactionType } from 'uniswap/src/features/transactions/types/transactionDetails' import { ONE_SECOND_MS } from 'utilities/src/time/time' -import { selectFinishedOnboarding } from 'wallet/src/features/wallet/selectors' +import { selectActiveAccountAddress, selectFinishedOnboarding } from 'wallet/src/features/wallet/selectors' +import { removeAccounts, setAccountAsActive } from 'wallet/src/features/wallet/slice' export function* pushNotificationsWatcherSaga() { yield* call(syncWithOneSignal) + yield* call(syncActiveWalletAddressTag) yield* takeEvery(initNotifsForNewUser.type, initNewUser) yield* takeEvery(updateNotifSettings.type, syncWithOneSignal) yield* takeEvery(finalizeTransaction.type, processFinalizedTx) + yield* takeEvery(setAccountAsActive.type, syncActiveWalletAddressTag) + yield* takeEvery(removeAccounts.type, syncActiveWalletAddressTag) } /** @@ -51,3 +55,12 @@ function* processFinalizedTx(action: ReturnType) { ) } } + +function* syncActiveWalletAddressTag() { + const activeAddress = yield* select(selectActiveAccountAddress) + if (activeAddress) { + yield* call(OneSignal.User.addTag, OneSignalUserTagField.ActiveWalletAddress, activeAddress) + } else { + yield* call(OneSignal.User.removeTag, OneSignalUserTagField.ActiveWalletAddress) + } +} diff --git a/apps/mobile/src/features/send/SendFlow.tsx b/apps/mobile/src/features/send/SendFlow.tsx index f8e1d17ce62..dabbd43c62f 100644 --- a/apps/mobile/src/features/send/SendFlow.tsx +++ b/apps/mobile/src/features/send/SendFlow.tsx @@ -12,11 +12,13 @@ import { SendReviewScreen } from 'src/features/send/SendReviewScreen' import { useWalletRestore } from 'src/features/wallet/useWalletRestore' import { ModalName, SectionName } from 'uniswap/src/features/telemetry/constants' import Trace from 'uniswap/src/features/telemetry/Trace' +import { TransactionSettingsStoreContextProvider } from 'uniswap/src/features/transactions/components/settings/stores/transactionSettingsStore/TransactionSettingsStoreContextProvider' import { TransactionModal } from 'uniswap/src/features/transactions/components/TransactionModal/TransactionModal' import { TransactionScreen, useTransactionModalContext, } from 'uniswap/src/features/transactions/components/TransactionModal/TransactionModalContext' +import { SwapFormStoreContextProvider } from 'uniswap/src/features/transactions/swap/stores/swapFormStore/SwapFormStoreContextProvider' import { SendContextProvider, useSendContext } from 'wallet/src/features/transactions/contexts/SendContext' export function SendFlow(): JSX.Element { @@ -45,9 +47,13 @@ export function SendFlow(): JSX.Element { walletNeedsRestore={walletNeedsRestore} onClose={onClose} > - - - + + + + + + + ) } diff --git a/apps/mobile/src/features/send/SendFormButton.tsx b/apps/mobile/src/features/send/SendFormButton.tsx index 82bf9ea84db..740f40d2e3d 100644 --- a/apps/mobile/src/features/send/SendFormButton.tsx +++ b/apps/mobile/src/features/send/SendFormButton.tsx @@ -8,7 +8,7 @@ import { AccountType } from 'uniswap/src/features/accounts/types' import { selectHasDismissedLowNetworkTokenWarning } from 'uniswap/src/features/behaviorHistory/selectors' import { UniswapEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' -import { useDismissedCompatibleAddressWarnings } from 'uniswap/src/features/tokens/slice/hooks' +import { useDismissedCompatibleAddressWarnings } from 'uniswap/src/features/tokens/warnings/slice/hooks' import { useTransactionModalContext } from 'uniswap/src/features/transactions/components/TransactionModal/TransactionModalContext' import { useIsBlocked } from 'uniswap/src/features/trm/hooks' import { TestID } from 'uniswap/src/test/fixtures/testIDs' diff --git a/apps/mobile/src/features/send/SendTokenForm.tsx b/apps/mobile/src/features/send/SendTokenForm.tsx index bc64573a93b..1fd1ae1690e 100644 --- a/apps/mobile/src/features/send/SendTokenForm.tsx +++ b/apps/mobile/src/features/send/SendTokenForm.tsx @@ -125,7 +125,7 @@ export function SendTokenForm(): JSX.Element { // Decimal pad logic const decimalPadRef = useRef(null) const maxDecimals = isFiatInput ? MAX_FIAT_INPUT_DECIMALS : (currencyIn?.decimals ?? 0) - const selectionRef = useRef() + const selectionRef = useRef(undefined) const onInputSelectionChange = useCallback( (start: number, end: number) => { @@ -295,7 +295,7 @@ export function SendTokenForm(): JSX.Element { style={StyleSheet.absoluteFill} > - + @@ -364,7 +364,11 @@ export function SendTokenForm(): JSX.Element { {!nftIn && ( <> - + { proposalNamespaces: mockNamespaces, chains: [UniverseChainId.Mainnet], dappRequestInfo, - verifyStatus: 'VERIFIED' as WalletConnectVerifyStatus, + verifyStatus: DappVerificationStatus.Verified, }, } @@ -140,25 +141,26 @@ describe('WalletConnect Saga', () => { .run() }) - it('falls back to dapp.url when verifyContext.verified.origin is not available', () => { - // Create a proposal without verified origin (null origin falls back to dapp.url) + it('dispatches addPendingSession with correct parameters for valid proposal', () => { + // Create a mock verification context for the verified status const mockVerifyContext: Verify.Context = { verified: { verifyUrl: 'https://verify.walletconnect.com', - validation: 'INVALID', - origin: null as unknown as string, // null origin should fallback to dapp.url + validation: 'VALID', + origin: 'https://valid-dapp.com', }, } + // Create a valid proposal with eip155 namespace const mockProposal = { - id: 890, + id: 456, proposer: { publicKey: 'test-public-key', metadata: { - name: 'Fallback Dapp', - description: 'Fallback Dapp Description', - url: 'https://fallback-dapp.com', // This should be used when verified origin is empty - icons: ['https://fallback-dapp.com/icon.png'], + name: 'Valid Dapp', + description: 'Valid Dapp Description', + url: 'https://valid-dapp.com', + icons: ['https://valid-dapp.com/icon.png'], }, }, relays: [], @@ -169,14 +171,14 @@ describe('WalletConnect Saga', () => { events: [], }, }, - pairingTopic: 'fallback-pairing-topic', + pairingTopic: 'valid-pairing-topic', expiryTimestamp: Date.now() + 1000 * 60 * 5, verifyContext: mockVerifyContext, } as unknown as ProposalTypes.Struct & { verifyContext?: Verify.Context } const activeAccountAddress = '0x1234567890abcdef' - // Mock namespaces + // Mock namespaces that would be returned by buildApprovedNamespaces const mockNamespaces = { eip155: { accounts: [`eip155:1:${activeAccountAddress}`], @@ -186,27 +188,29 @@ describe('WalletConnect Saga', () => { }, } + // Mock the buildApprovedNamespaces function to return our mock namespaces const buildApprovedNamespacesMock = buildApprovedNamespaces as jest.Mock buildApprovedNamespacesMock.mockReturnValue(mockNamespaces) - // Mock parseVerifyStatus to return INVALID for this test + // Mock parseVerifyStatus to return VERIFIED for this test const parseVerifyStatusMock = parseVerifyStatus as jest.Mock - parseVerifyStatusMock.mockReturnValue('INVALID') + parseVerifyStatusMock.mockReturnValue('VERIFIED') + // Create properly typed dappRequestInfo const dappRequestInfo: DappRequestInfo = { - name: 'Fallback Dapp', - url: 'https://fallback-dapp.com', // Should use dapp.url when verified origin is null - icon: 'https://fallback-dapp.com/icon.png', + name: 'Valid Dapp', + url: 'https://valid-dapp.com', + icon: 'https://valid-dapp.com/icon.png', requestType: DappRequestType.WalletConnectSessionRequest, } const expectedPendingSession = { wcSession: { - id: '890', + id: '456', proposalNamespaces: mockNamespaces, chains: [UniverseChainId.Mainnet], dappRequestInfo, - verifyStatus: 'INVALID' as WalletConnectVerifyStatus, + verifyStatus: DappVerificationStatus.Verified, }, } @@ -216,6 +220,7 @@ describe('WalletConnect Saga', () => { if (selector === selectActiveAccountAddress) { return activeAccountAddress } + // For any other selectors that might access wallet state return next() }, }) @@ -231,26 +236,17 @@ describe('WalletConnect Saga', () => { .run() }) - it('dispatches addPendingSession with correct parameters for valid proposal', () => { - // Create a mock verification context for the verified status - const mockVerifyContext: Verify.Context = { - verified: { - verifyUrl: 'https://verify.walletconnect.com', - validation: 'VALID', - origin: 'https://valid-dapp.com', - }, - } - - // Create a valid proposal with eip155 namespace + it('falls back to dapp.url when verifyContext.verified.origin is not available', () => { + // Create a proposal WITHOUT verifyContext.verified.origin const mockProposal = { - id: 456, + id: 999, proposer: { publicKey: 'test-public-key', metadata: { - name: 'Valid Dapp', - description: 'Valid Dapp Description', - url: 'https://valid-dapp.com', - icons: ['https://valid-dapp.com/icon.png'], + name: 'Fallback Dapp', + description: 'Fallback Dapp Description', + url: 'https://fallback-dapp.com', + icons: ['https://fallback-dapp.com/icon.png'], }, }, relays: [], @@ -261,9 +257,9 @@ describe('WalletConnect Saga', () => { events: [], }, }, - pairingTopic: 'valid-pairing-topic', + pairingTopic: 'fallback-pairing-topic', expiryTimestamp: Date.now() + 1000 * 60 * 5, - verifyContext: mockVerifyContext, + // No verifyContext provided } as unknown as ProposalTypes.Struct & { verifyContext?: Verify.Context } const activeAccountAddress = '0x1234567890abcdef' @@ -282,25 +278,25 @@ describe('WalletConnect Saga', () => { const buildApprovedNamespacesMock = buildApprovedNamespaces as jest.Mock buildApprovedNamespacesMock.mockReturnValue(mockNamespaces) - // Mock parseVerifyStatus to return VERIFIED for this test + // Mock parseVerifyStatus to return UNVERIFIED when no verifyContext const parseVerifyStatusMock = parseVerifyStatus as jest.Mock - parseVerifyStatusMock.mockReturnValue('VERIFIED') + parseVerifyStatusMock.mockReturnValue('UNVERIFIED') - // Create properly typed dappRequestInfo + // Create properly typed dappRequestInfo - should use dapp.url as fallback const dappRequestInfo: DappRequestInfo = { - name: 'Valid Dapp', - url: 'https://valid-dapp.com', - icon: 'https://valid-dapp.com/icon.png', + name: 'Fallback Dapp', + url: 'https://fallback-dapp.com', // Should fallback to dapp.url + icon: 'https://fallback-dapp.com/icon.png', requestType: DappRequestType.WalletConnectSessionRequest, } const expectedPendingSession = { wcSession: { - id: '456', + id: '999', proposalNamespaces: mockNamespaces, chains: [UniverseChainId.Mainnet], dappRequestInfo, - verifyStatus: 'VERIFIED' as WalletConnectVerifyStatus, + verifyStatus: DappVerificationStatus.Unverified, }, } @@ -310,7 +306,6 @@ describe('WalletConnect Saga', () => { if (selector === selectActiveAccountAddress) { return activeAccountAddress } - // For any other selectors that might access wallet state return next() }, }) diff --git a/apps/mobile/src/features/walletConnect/signWcRequestSaga.ts b/apps/mobile/src/features/walletConnect/signWcRequestSaga.ts index f52cf35c930..1efd04ca0f0 100644 --- a/apps/mobile/src/features/walletConnect/signWcRequestSaga.ts +++ b/apps/mobile/src/features/walletConnect/signWcRequestSaga.ts @@ -137,7 +137,15 @@ function* signWcRequest(params: SignMessageParams | SignTransactionParams) { } const { transactionHash } = yield* call(executeTransaction, txParams) - result = { id: params.request.id, capabilities: {} } + result = { + id: params.request.id, + capabilities: { + caip345: { + caip2: `eip155:${params.request.chainId}`, + transactionHashes: [transactionHash], + }, + }, + } // Store the batch transaction in Redux yield* put( diff --git a/apps/mobile/src/features/walletConnect/utils.test.ts b/apps/mobile/src/features/walletConnect/utils.test.ts index 275dba4fe5e..7969c56e888 100644 --- a/apps/mobile/src/features/walletConnect/utils.test.ts +++ b/apps/mobile/src/features/walletConnect/utils.test.ts @@ -1,5 +1,6 @@ import { utils } from 'ethers' import { + convertCapabilitiesToScopedProperties, decodeMessage, getAccountAddressFromEIP155String, getChainIdFromEIP155String, @@ -499,3 +500,87 @@ describe(parseGetCallsStatusRequest, () => { }) }) }) + +describe(convertCapabilitiesToScopedProperties, () => { + it('converts single chain capability to CAIP-2 format', () => { + const capabilities = { + '0xa': { + atomic: { status: 'supported' }, + }, + } + + const result = convertCapabilitiesToScopedProperties(capabilities) + + expect(result).toEqual({ + 'eip155:10': { + atomic: { status: 'supported' }, + }, + }) + }) + + it('converts multiple chain capabilities to CAIP-2 format', () => { + const capabilities = { + '0x1': { + atomic: { status: 'supported' }, + }, + '0x89': { + atomic: { status: 'unsupported' }, + }, + '0xa': { + atomic: { status: 'supported' }, + paymasterService: { supported: true }, + }, + } + + const result = convertCapabilitiesToScopedProperties(capabilities) + + expect(result).toEqual({ + 'eip155:1': { + atomic: { status: 'supported' }, + }, + 'eip155:137': { + atomic: { status: 'unsupported' }, + }, + 'eip155:10': { + atomic: { status: 'supported' }, + paymasterService: { supported: true }, + }, + }) + }) + + it('returns empty object when given empty capabilities', () => { + const capabilities = {} + + const result = convertCapabilitiesToScopedProperties(capabilities) + + expect(result).toEqual({}) + }) + + it('handles invalid hex chain IDs that cause errors', () => { + const capabilities = { + '0x1': { + atomic: { status: 'supported' }, + }, + 'invalid-hex': { + atomic: { status: 'unsupported' }, + }, + '0x89': { + atomic: { status: 'supported' }, + }, + } + + const result = convertCapabilitiesToScopedProperties(capabilities) + + // hexToNumber returns NaN for invalid hex, which should be excluded + // The function continues execution despite the invalid chain ID + expect(result).toHaveProperty('eip155:1') + expect(result).toHaveProperty('eip155:137') + expect(result).not.toHaveProperty('eip155:NaN') + expect(result['eip155:1']).toEqual({ + atomic: { status: 'supported' }, + }) + expect(result['eip155:137']).toEqual({ + atomic: { status: 'supported' }, + }) + }) +}) diff --git a/apps/mobile/src/features/walletConnect/utils.ts b/apps/mobile/src/features/walletConnect/utils.ts index 5a5cab9f75e..685871502dc 100644 --- a/apps/mobile/src/features/walletConnect/utils.ts +++ b/apps/mobile/src/features/walletConnect/utils.ts @@ -5,7 +5,6 @@ import { wcWeb3Wallet } from 'src/features/walletConnect/walletConnectClient' import { SignRequest, TransactionRequest, - WalletConnectVerifyStatus, WalletGetCallsStatusRequest, WalletGetCapabilitiesRequest, WalletSendCallsRequest, @@ -15,8 +14,15 @@ import { toSupportedChainId } from 'uniswap/src/features/chains/utils' import { EthMethod, EthSignMethod, WalletConnectEthMethod } from 'uniswap/src/features/dappRequests/types' import { DappRequestInfo, DappRequestType } from 'uniswap/src/types/walletConnect' import { hexToNumber } from 'utilities/src/addresses/hex' +import { logger } from 'utilities/src/logger/logger' import { generateBatchId } from 'wallet/src/features/batchedTransactions/utils' -import { GetCallsStatusParams, SendCallsParams } from 'wallet/src/features/dappRequests/types' +import { + Capability, + DappVerificationStatus, + GetCallsStatusParams, + SendCallsParams, +} from 'wallet/src/features/dappRequests/types' + /** * Construct WalletConnect 2.0 session namespaces to complete a new pairing. Used when approving a new pairing request. * Assumes each namespace has been validated and is supported by the app with `validateProposalNamespaces()`. @@ -352,23 +358,65 @@ export async function pairWithWalletConnectURI(uri: string): Promise, +): Record> { + const scopedProperties: Record> = {} + + for (const [hexChainId, capability] of Object.entries(capabilities)) { + try { + const chainId = hexToNumber(hexChainId) + if (isNaN(chainId)) { + continue + } + const caip2Key = `eip155:${chainId}` + scopedProperties[caip2Key] = capability + } catch (error) { + logger.error(error, { + tags: { function: 'convertCapabilitiesToScopedProperties', file: 'walletConnect/utils.ts' }, + }) + continue + } + } + + return scopedProperties } diff --git a/apps/mobile/src/features/walletConnect/walletConnectSlice.ts b/apps/mobile/src/features/walletConnect/walletConnectSlice.ts index 7184969625c..6acc756db31 100644 --- a/apps/mobile/src/features/walletConnect/walletConnectSlice.ts +++ b/apps/mobile/src/features/walletConnect/walletConnectSlice.ts @@ -4,20 +4,14 @@ import { UniverseChainId } from 'uniswap/src/features/chains/types' import { EthMethod, EthSignMethod } from 'uniswap/src/features/dappRequests/types' import { DappRequestInfo, EthTransaction, UwULinkMethod } from 'uniswap/src/types/walletConnect' import { logger } from 'utilities/src/logger/logger' -import { Call, Capability } from 'wallet/src/features/dappRequests/types' - -export enum WalletConnectVerifyStatus { - Verified = 'VERIFIED', - Unverified = 'UNVERIFIED', - Threat = 'THREAT', -} +import { Call, Capability, DappVerificationStatus } from 'wallet/src/features/dappRequests/types' export type WalletConnectPendingSession = { id: string chains: UniverseChainId[] dappRequestInfo: DappRequestInfo proposalNamespaces: ProposalTypes.OptionalNamespaces - verifyStatus: WalletConnectVerifyStatus + verifyStatus: DappVerificationStatus } export type WalletConnectSession = { @@ -31,6 +25,13 @@ export type WalletConnectSession = { * is tracking as the active account based on session events (approve session, change account, etc). */ activeAccount: string + + /** + * EIP-5792 capabilities for this session, stored in hex chainId format. + * Contains atomic batch support status per chain. + * Only populated if EIP-5792 feature flag was enabled during session approval. + */ + capabilities?: Record } interface BaseRequest { @@ -99,9 +100,16 @@ export type WalletConnectSigningRequest = | UwuLinkErc20Request | WalletSendCallsEncodedRequest +type PersonalSignRequest = SignRequest & { + type: EthMethod.PersonalSign | EthMethod.EthSign +} + export const isTransactionRequest = (request: WalletConnectSigningRequest): request is TransactionRequest => request.type === EthMethod.EthSendTransaction || request.type === UwULinkMethod.Erc20Send +export const isPersonalSignRequest = (request: WalletConnectSigningRequest): request is PersonalSignRequest => + request.type === EthMethod.PersonalSign || request.type === EthMethod.EthSign + export const isBatchedTransactionRequest = ( request: WalletConnectSigningRequest, ): request is WalletSendCallsEncodedRequest => request.type === EthMethod.WalletSendCalls diff --git a/apps/mobile/src/screens/DevScreen.tsx b/apps/mobile/src/screens/DevScreen.tsx index 78f6bd38e9f..020b1a6d33c 100644 --- a/apps/mobile/src/screens/DevScreen.tsx +++ b/apps/mobile/src/screens/DevScreen.tsx @@ -14,7 +14,7 @@ import { resetDismissedBridgedAssetWarnings, resetDismissedCompatibleAddressWarnings, resetDismissedWarnings, -} from 'uniswap/src/features/tokens/slice/slice' +} from 'uniswap/src/features/tokens/warnings/slice/slice' import { useAppInsets } from 'uniswap/src/hooks/useAppInsets' import { MobileScreens } from 'uniswap/src/types/screens/mobile' import { setClipboard } from 'uniswap/src/utils/clipboard' diff --git a/apps/mobile/src/screens/ExploreScreen.tsx b/apps/mobile/src/screens/ExploreScreen.tsx index 136fc7bcaa6..f3f16e5283f 100644 --- a/apps/mobile/src/screens/ExploreScreen.tsx +++ b/apps/mobile/src/screens/ExploreScreen.tsx @@ -1,4 +1,5 @@ -import { useIsFocused, useNavigation, useScrollToTop } from '@react-navigation/native' +import type { RouteProp } from '@react-navigation/native' +import { useIsFocused, useNavigation, useRoute, useScrollToTop } from '@react-navigation/native' import { SharedEventName } from '@uniswap/analytics-events' import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { useEffect, useRef, useState } from 'react' @@ -8,6 +9,7 @@ import type { FlatList } from 'react-native-gesture-handler' import { useAnimatedRef } from 'react-native-reanimated' import type { Edge } from 'react-native-safe-area-context' import { useDispatch } from 'react-redux' +import type { ExploreStackParamList } from 'src/app/navigation/types' import { ExploreSections } from 'src/components/explore/ExploreSections/ExploreSections' import { ExploreScreenSearchResultsList } from 'src/components/explore/search/ExploreScreenSearchResultsList' import { Screen } from 'src/components/layout/Screen' @@ -38,6 +40,9 @@ export function ExploreScreen(): JSX.Element { const { chains } = useEnabledChains() const isBottomTabsEnabled = useFeatureFlag(FeatureFlags.BottomTabs) const navigation = useNavigation() + const route = useRoute>() + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- route.params can be null + const { chainId, orderByMetric, showFavorites } = route.params ?? {} const { isSheetReady } = useBottomSheetContext({ forceSafeReturn: isBottomTabsEnabled }) @@ -53,6 +58,8 @@ export function ExploreScreen(): JSX.Element { // Use refs to avoid stale closures in the event listener const isAtTopRef = useRef(isAtTop) const isFocusedRef = useRef(isFocused) + // Track the previous route name to detect true double-tap behavior + const prevRouteNameRef = useRef(null) isAtTopRef.current = isAtTop isFocusedRef.current = isFocused @@ -76,18 +83,25 @@ export function ExploreScreen(): JSX.Element { const unsubscribe = navigation.addListener('state', (e) => { const currentRouteName = e.data.state.routeNames[e.data.state.index] as unknown as string | undefined - - // Check if we're navigating to the Explore screen const isOnExploreScreen = currentRouteName === MobileScreens.Explore - // Only handle this if: - // 1. We were already focused before the state change (i.e., tab was pressed while already on this screen) - // 2. The current route is the Explore screen - // 3. The screen is currently focused - if (!isOnExploreScreen || !isFocusedRef.current) { + // Double-tap detection: Only trigger focus when user taps Explore tab while already on Explore + // This distinguishes between: + // - Initial navigation to Explore (prevRoute !== Explore) → No auto-focus + // - Tab double-tap (prevRoute === Explore && currentRoute === Explore) → Focus search + const isDoubleTap = prevRouteNameRef.current === MobileScreens.Explore && isOnExploreScreen + + // Update the previous route for next navigation event + prevRouteNameRef.current = currentRouteName ?? null + + // Only handle double-tap behavior when: + // 1. This is a true double-tap (was on Explore, tapped Explore again) + // 2. The screen is currently focused + if (!isDoubleTap || !isFocusedRef.current) { return } + // Double-tap behavior: Focus search if at top, scroll to top otherwise if (isAtTopRef.current) { textInputRef.current?.focus() } else { @@ -103,7 +117,7 @@ export function ExploreScreen(): JSX.Element { const canRenderList = useRenderNextFrame(isSheetReady && !isSearchMode) const { onChangeChainFilter, onChangeText, searchFilter, chainFilter, parsedChainFilter, parsedSearchFilter } = - useFilterCallbacks(null, ModalName.Search) + useFilterCallbacks(chainId ?? null, ModalName.Search) const onSearchChangeText = useEvent((newSearchFilter: string): void => { onChangeText(newSearchFilter) @@ -144,6 +158,7 @@ export function ExploreScreen(): JSX.Element { ) : isSheetReady && canRenderList ? ( - + ) : null} ) @@ -187,7 +208,7 @@ export function ExploreScreen(): JSX.Element { */ const useRenderNextFrame = (condition: boolean): boolean => { const [canRender, setCanRender] = useState(false) - const rafRef = useRef() + const rafRef = useRef(undefined) const mountedRef = useRef(true) const conditionRef = useRef(condition) diff --git a/apps/mobile/src/screens/FiatOnRampScreen.tsx b/apps/mobile/src/screens/FiatOnRampScreen.tsx index 7a69327b7f9..3efcf35a2d1 100644 --- a/apps/mobile/src/screens/FiatOnRampScreen.tsx +++ b/apps/mobile/src/screens/FiatOnRampScreen.tsx @@ -121,7 +121,7 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element { const [selectingCountry, setSelectingCountry] = useState(false) const [decimalPadReady, setDecimalPadReady] = useState(false) const decimalPadRef = useRef(null) - const selectionRef = useRef() + const selectionRef = useRef(undefined) const amountUpdatedTimeRef = useRef(0) const [value, setValue] = useState('') const valueRef = useRef('') @@ -566,6 +566,7 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element { id={DecimalPadCalculatedSpaceId.FiatOnRamp} decimalPadRef={decimalPadRef} additionalElementsHeight={DECIMAL_PAD_EXTRA_ELEMENTS_HEIGHT} + isDecimalPadReady={decimalPadReady} /> + diff --git a/apps/mobile/src/screens/Import/__snapshots__/RestoreCloudBackupPasswordScreen.test.tsx.snap b/apps/mobile/src/screens/Import/__snapshots__/RestoreCloudBackupPasswordScreen.test.tsx.snap index 2ba8b7f5615..e5a5af4bcc8 100644 --- a/apps/mobile/src/screens/Import/__snapshots__/RestoreCloudBackupPasswordScreen.test.tsx.snap +++ b/apps/mobile/src/screens/Import/__snapshots__/RestoreCloudBackupPasswordScreen.test.tsx.snap @@ -4,12 +4,7 @@ exports[`RestoreCloudBackupPasswordScreen renders correctly 1`] = ` { - if (view !== View.SeedPhrase) { - return undefined - } + useFocusEffect( + useCallback(() => { + if (view !== View.SeedPhrase) { + return undefined + } - const listener = addScreenshotListener(() => - navigate(ModalName.ScreenshotWarning, { acknowledgeText: t('common.button.ok') }), - ) - return () => listener.remove() - }, [view, t]) + const listener = addScreenshotListener(() => { + navigate(ModalName.ScreenshotWarning, { acknowledgeText: t('common.button.ok') }) + }) + return () => listener.remove() + }, [view, t]), + ) useEffect(() => { if (confirmContinueButtonPressed && hasBackup(BackupType.Manual, account)) { diff --git a/apps/mobile/src/screens/Onboarding/TermsOfService.tsx b/apps/mobile/src/screens/Onboarding/TermsOfService.tsx index cf8623168c3..8913c9cb725 100644 --- a/apps/mobile/src/screens/Onboarding/TermsOfService.tsx +++ b/apps/mobile/src/screens/Onboarding/TermsOfService.tsx @@ -10,6 +10,7 @@ export function TermsOfService(): JSX.Element { components={{ highlightTerms: ( => openUri({ uri: uniswapUrls.termsOfServiceUrl })} @@ -17,6 +18,7 @@ export function TermsOfService(): JSX.Element { ), highlightPrivacy: ( => openUri({ uri: uniswapUrls.privacyPolicyUrl })} diff --git a/apps/mobile/src/screens/Onboarding/__snapshots__/BackupScreen.test.tsx.snap b/apps/mobile/src/screens/Onboarding/__snapshots__/BackupScreen.test.tsx.snap index 167f5fdd4ad..126056f6548 100644 --- a/apps/mobile/src/screens/Onboarding/__snapshots__/BackupScreen.test.tsx.snap +++ b/apps/mobile/src/screens/Onboarding/__snapshots__/BackupScreen.test.tsx.snap @@ -4,12 +4,7 @@ exports[`BackupScreen renders backup options when none are completed 1`] = ` diff --git a/apps/mobile/src/screens/SettingsScreen.tsx b/apps/mobile/src/screens/SettingsScreen.tsx index 8f4d3e83b9c..6fa0cf28f23 100644 --- a/apps/mobile/src/screens/SettingsScreen.tsx +++ b/apps/mobile/src/screens/SettingsScreen.tsx @@ -226,7 +226,7 @@ export function SettingsScreen(): JSX.Element { }, { navigationModal: ModalName.PortfolioBalanceModal, - text: t('settings.setting.smallBalances.title'), + text: t('settings.setting.balancesActivity.title'), icon: , }, { diff --git a/apps/mobile/src/screens/TokenDetailsHeaders.tsx b/apps/mobile/src/screens/TokenDetailsHeaders.tsx index ba0b0744664..66cadffd71e 100644 --- a/apps/mobile/src/screens/TokenDetailsHeaders.tsx +++ b/apps/mobile/src/screens/TokenDetailsHeaders.tsx @@ -1,6 +1,8 @@ import React, { memo } from 'react' import { useTranslation } from 'react-i18next' import { FadeIn } from 'react-native-reanimated' +import { MODAL_OPEN_WAIT_TIME } from 'src/app/navigation/constants' +import { navigate } from 'src/app/navigation/rootNavigation' import { useTokenDetailsContext } from 'src/components/TokenDetails/TokenDetailsContext' import { TokenDetailsFavoriteButton } from 'src/components/TokenDetails/TokenDetailsFavoriteButton' import { useTokenDetailsCurrentChainBalance } from 'src/components/TokenDetails/useTokenDetailsCurrentChainBalance' @@ -21,7 +23,9 @@ import { TokenMenuActionType, useTokenContextMenuOptions, } from 'uniswap/src/features/portfolio/balances/hooks/useTokenContextMenuOptions' +import { ModalName } from 'uniswap/src/features/telemetry/constants' import { TestID } from 'uniswap/src/test/fixtures/testIDs' +import { useEvent } from 'utilities/src/react/hooks' import { useBooleanState } from 'utilities/src/react/useBooleanState' export const HeaderTitleElement = memo(function HeaderTitleElement(): JSX.Element { @@ -62,6 +66,22 @@ export const HeaderRightElement = memo(function HeaderRightElement(): JSX.Elemen useTokenDetailsContext() const currentChainBalance = useTokenDetailsCurrentChainBalance() + const openReportTokenModal = useEvent(() => { + setTimeout(() => { + navigate(ModalName.ReportTokenIssue, { + source: 'token-details', + currency: currencyInfo?.currency, + isMarkedSpam: currencyInfo?.isSpam, + }) + }, MODAL_OPEN_WAIT_TIME) + }) + + const openReportDataIssueModal = useEvent(() => { + setTimeout(() => { + navigate(ModalName.ReportTokenData, { currency: currencyInfo?.currency, isMarkedSpam: currencyInfo?.isSpam }) + }, MODAL_OPEN_WAIT_TIME) + }) + const { value: isOpen, setTrue: openMenu, setFalse: closeMenu } = useBooleanState(false) const menuActions = useTokenContextMenuOptions({ excludedActions: EXCLUDED_ACTIONS, @@ -70,6 +90,8 @@ export const HeaderRightElement = memo(function HeaderRightElement(): JSX.Elemen tokenSymbolForNotification: currencyInfo?.currency.symbol, portfolioBalance: currentChainBalance, openContractAddressExplainerModal, + openReportDataIssueModal, + openReportTokenModal, copyAddressToClipboard, closeMenu: () => {}, }) diff --git a/apps/mobile/src/screens/TokenDetailsScreen.tsx b/apps/mobile/src/screens/TokenDetailsScreen.tsx index 83a20f7ec10..63e3176d019 100644 --- a/apps/mobile/src/screens/TokenDetailsScreen.tsx +++ b/apps/mobile/src/screens/TokenDetailsScreen.tsx @@ -1,9 +1,12 @@ import { useApolloClient } from '@apollo/client' import { ReactNavigationPerformanceView } from '@shopify/react-native-performance-navigation' import { GQLQueries, GraphQLApi } from '@universe/api' -import React, { memo, useCallback, useEffect, useMemo } from 'react' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' +import React, { memo, useEffect, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { FadeInDown, FadeOutDown } from 'react-native-reanimated' +import { useDispatch } from 'react-redux' +import { MODAL_OPEN_WAIT_TIME } from 'src/app/navigation/constants' import { navigate } from 'src/app/navigation/rootNavigation' import type { AppStackScreenProp } from 'src/app/navigation/types' import { HeaderScrollScreen } from 'src/components/layout/screens/HeaderScrollScreen' @@ -21,11 +24,14 @@ import { useTokenDetailsCTAVariant } from 'src/components/TokenDetails/useTokenD import { useTokenDetailsCurrentChainBalance } from 'src/components/TokenDetails/useTokenDetailsCurrentChainBalance' import { HeaderRightElement, HeaderTitleElement } from 'src/screens/TokenDetailsHeaders' import { useIsScreenNavigationReady } from 'src/utils/useIsScreenNavigationReady' -import { Flex, Separator } from 'ui/src' +import { Flex, Separator, Text } from 'ui/src' import { ArrowDownCircle, ArrowUpCircle, Bank, SendRoundedAirplane } from 'ui/src/components/icons' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard' import type { MenuOptionItem } from 'uniswap/src/components/menus/ContextMenuV2' +import { WarningSeverity } from 'uniswap/src/components/modals/WarningModal/types' +import { WarningModal } from 'uniswap/src/components/modals/WarningModal/WarningModal' +import { LearnMoreLink } from 'uniswap/src/components/text/LearnMoreLink' import { PollingInterval } from 'uniswap/src/constants/misc' import { useCrossChainBalances } from 'uniswap/src/data/balances/hooks/useCrossChainBalances' import { @@ -38,12 +44,16 @@ import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledCh import { TokenList } from 'uniswap/src/features/dataApi/types' import { currencyIdToContractInput } from 'uniswap/src/features/dataApi/utils/currencyIdToContractInput' import { useIsSupportedFiatOnRampCurrency } from 'uniswap/src/features/fiatOnRamp/hooks' +import { pushNotification } from 'uniswap/src/features/notifications/slice/slice' +import { AppNotificationType } from 'uniswap/src/features/notifications/slice/types' import { useOnChainNativeCurrencyBalance } from 'uniswap/src/features/portfolio/api' import { ModalName } from 'uniswap/src/features/telemetry/constants' import Trace from 'uniswap/src/features/telemetry/Trace' -import { TokenWarningCard } from 'uniswap/src/features/tokens/TokenWarningCard' -import TokenWarningModal from 'uniswap/src/features/tokens/TokenWarningModal' +import { TokenWarningCard } from 'uniswap/src/features/tokens/warnings/TokenWarningCard' +import TokenWarningModal from 'uniswap/src/features/tokens/warnings/TokenWarningModal' +import { AZTEC_URL } from 'uniswap/src/features/transactions/swap/hooks/useSwapWarnings/getAztecUnavailableWarning' import { useAppInsets } from 'uniswap/src/hooks/useAppInsets' +import { useShouldShowAztecWarning } from 'uniswap/src/hooks/useShouldShowAztecWarning' import type { CurrencyField } from 'uniswap/src/types/currency' import { MobileScreens } from 'uniswap/src/types/screens/mobile' import { AddressStringFormat, normalizeAddress } from 'uniswap/src/utils/addresses' @@ -151,12 +161,12 @@ const TokenDetailsErrorCard = memo(function _TokenDetailsErrorCard(): JSX.Elemen const apolloClient = useApolloClient() const { error, setError } = useTokenDetailsContext() - const onRetry = useCallback(() => { + const onRetry = useEvent(() => { setError(undefined) apolloClient .refetchQueries({ include: [GQLQueries.TokenDetailsScreen, GQLQueries.TokenPriceHistory] }) .catch((e) => setError(e)) - }, [apolloClient, setError]) + }) return error ? ( @@ -166,7 +176,10 @@ const TokenDetailsErrorCard = memo(function _TokenDetailsErrorCard(): JSX.Elemen }) const TokenDetailsModals = memo(function _TokenDetailsModals(): JSX.Element { + const { t } = useTranslation() + const dispatch = useDispatch() const { navigateToSwapFlow } = useWalletNavigation() + const isAztecDisabled = useFeatureFlag(FeatureFlags.DisableAztecToken) const { chainId, @@ -175,15 +188,13 @@ const TokenDetailsModals = memo(function _TokenDetailsModals(): JSX.Element { currencyInfo, isTokenWarningModalOpen, isContractAddressExplainerModalOpen, + isAztecWarningModalOpen, closeTokenWarningModal, closeContractAddressExplainerModal, + closeAztecWarningModal, copyAddressToClipboard, } = useTokenDetailsContext() - const onCloseTokenWarning = useEvent(() => { - closeTokenWarningModal() - }) - const onAcknowledgeTokenWarning = useEvent(() => { closeTokenWarningModal() if (activeTransactionType !== undefined) { @@ -198,6 +209,15 @@ const TokenDetailsModals = memo(function _TokenDetailsModals(): JSX.Element { } }) + const onTokenWarningReportSuccess = useEvent(() => { + dispatch( + pushNotification({ + type: AppNotificationType.Success, + title: t('common.reported'), + }), + ) + }) + return ( <> {isTokenWarningModalOpen && currencyInfo && ( @@ -205,7 +225,8 @@ const TokenDetailsModals = memo(function _TokenDetailsModals(): JSX.Element { isInfoOnlyWarning currencyInfo0={currencyInfo} isVisible={isTokenWarningModalOpen} - closeModalOnly={onCloseTokenWarning} + closeModalOnly={closeTokenWarningModal} + onReportSuccess={onTokenWarningReportSuccess} onAcknowledge={onAcknowledgeTokenWarning} /> )} @@ -213,6 +234,26 @@ const TokenDetailsModals = memo(function _TokenDetailsModals(): JSX.Element { {isContractAddressExplainerModalOpen && ( )} + + {isAztecWarningModalOpen && isAztecDisabled && ( + + + {t('swap.warning.aztecUnavailable.message')} + + + + } + acknowledgeText={t('common.button.close')} + onClose={closeAztecWarningModal} + onAcknowledge={closeAztecWarningModal} + /> + )} ) }) @@ -223,8 +264,19 @@ const TokenDetailsActionButtonsWrapper = memo(function _TokenDetailsActionButton const activeAddress = useActiveAccountAddressWithThrow() const { isTestnetModeEnabled } = useEnabledChains() - const { currencyId, chainId, address, currencyInfo, openTokenWarningModal, tokenColorLoading, navigation } = - useTokenDetailsContext() + const { + currencyId, + chainId, + address, + currencyInfo, + openTokenWarningModal, + openAztecWarningModal, + tokenColorLoading, + navigation, + } = useTokenDetailsContext() + const showAztecWarning = useShouldShowAztecWarning( + currencyInfo?.currency.isToken ? currencyInfo.currency.address : '', + ) const { navigateToFiatOnRamp, navigateToSwapFlow, navigateToSend, navigateToReceive } = useWalletNavigation() @@ -258,42 +310,50 @@ const TokenDetailsActionButtonsWrapper = memo(function _TokenDetailsActionButton currencyChainId: chainId, }) - const onPressSwap = useCallback( - (currencyField: CurrencyField) => { - if (isBlocked) { - openTokenWarningModal() - } else { - navigateToSwapFlow({ currencyField, currencyAddress: address, currencyChainId: chainId }) - } - }, - [isBlocked, openTokenWarningModal, navigateToSwapFlow, address, chainId], - ) + const onPressSwap = useEvent((currencyField: CurrencyField) => { + if (showAztecWarning) { + openAztecWarningModal() + } else if (isBlocked) { + openTokenWarningModal() + } else { + navigateToSwapFlow({ currencyField, currencyAddress: address, currencyChainId: chainId }) + } + }) - const onPressBuyFiatOnRamp = useCallback( - (isOfframp = false): void => { + const onPressBuyFiatOnRamp = useEvent((isOfframp: boolean = false): void => { + if (showAztecWarning) { + openAztecWarningModal() + } else { navigateToFiatOnRamp({ prefilledCurrency: fiatOnRampCurrency, isOfframp }) - }, - [navigateToFiatOnRamp, fiatOnRampCurrency], - ) + } + }) - const onPressGet = useCallback(() => { - navigate(ModalName.BuyNativeToken, { - chainId, - currencyId, - }) - }, [chainId, currencyId]) + const onPressGet = useEvent(() => { + if (showAztecWarning) { + openAztecWarningModal() + } else { + navigate(ModalName.BuyNativeToken, { + chainId, + currencyId, + }) + } + }) - const onPressSend = useCallback(() => { - navigateToSend({ currencyAddress: address, chainId }) - }, [address, chainId, navigateToSend]) + const onPressSend = useEvent(() => { + if (showAztecWarning) { + openAztecWarningModal() + } else { + navigateToSend({ currencyAddress: address, chainId }) + } + }) - const onPressWithdraw = useCallback(() => { + const onPressWithdraw = useEvent(() => { setTimeout(() => { navigate(ModalName.Wormhole, { currencyInfo, }) - }, 300) // delay is needed to prevent menu from not closing properly - }, [currencyInfo]) + }, MODAL_OPEN_WAIT_TIME) + }) const bridgedWithdrawalInfo = currencyInfo?.bridgedWithdrawalInfo @@ -316,7 +376,12 @@ const TokenDetailsActionButtonsWrapper = memo(function _TokenDetailsActionButton const actions: MenuOptionItem[] = [] if (fiatOnRampCurrency) { - actions.push({ label: t('common.button.buy'), Icon: Bank, onPress: () => onPressBuyFiatOnRamp() }) + actions.push({ + label: t('common.button.buy'), + Icon: Bank, + onPress: () => onPressBuyFiatOnRamp(), + disabled: showAztecWarning, + }) } if (bridgedWithdrawalInfo && hasTokenBalance) { @@ -331,7 +396,12 @@ const TokenDetailsActionButtonsWrapper = memo(function _TokenDetailsActionButton } if (hasTokenBalance && fiatOnRampCurrency) { - actions.push({ label: t('common.button.sell'), Icon: ArrowUpCircle, onPress: () => onPressBuyFiatOnRamp(true) }) + actions.push({ + label: t('common.button.sell'), + Icon: ArrowUpCircle, + onPress: () => onPressBuyFiatOnRamp(true), + disabled: showAztecWarning, + }) } if (hasTokenBalance) { @@ -347,6 +417,7 @@ const TokenDetailsActionButtonsWrapper = memo(function _TokenDetailsActionButton t, bridgedWithdrawalInfo, hasTokenBalance, + showAztecWarning, onPressWithdraw, onPressSend, navigateToReceive, @@ -368,13 +439,15 @@ const TokenDetailsActionButtonsWrapper = memo(function _TokenDetailsActionButton actionMenuOptions={actionMenuOptions} userHasBalance={hasTokenBalance} onPressDisabled={ - isTestnetModeEnabled - ? (): void => - navigate(ModalName.TestnetMode, { - unsupported: true, - descriptionCopy: t('tdp.noTestnetSupportDescription'), - }) - : openTokenWarningModal + showAztecWarning + ? openAztecWarningModal + : isTestnetModeEnabled + ? (): void => + navigate(ModalName.TestnetMode, { + unsupported: true, + descriptionCopy: t('tdp.noTestnetSupportDescription'), + }) + : openTokenWarningModal } /> diff --git a/apps/mobile/src/test/fixtures/redux.ts b/apps/mobile/src/test/fixtures/redux.ts index 1b1d8b45b36..ef3153c669c 100644 --- a/apps/mobile/src/test/fixtures/redux.ts +++ b/apps/mobile/src/test/fixtures/redux.ts @@ -14,7 +14,14 @@ type PreloadedMobileStateOptions = { account: Account | undefined } -export const preloadedMobileState = createFixture, PreloadedMobileStateOptions>({ +type PreloadedMobileStateFactory = ( + overrides?: Partial & PreloadedMobileStateOptions>, +) => PreloadedState + +export const preloadedMobileState: PreloadedMobileStateFactory = createFixture< + PreloadedState, + PreloadedMobileStateOptions +>({ account: undefined, })(({ account }) => ({ ...preloadedWalletPackageState({ account }), diff --git a/apps/mobile/src/utils/hooks.ts b/apps/mobile/src/utils/hooks.ts index 157e3e630de..1bb4493e8a0 100644 --- a/apps/mobile/src/utils/hooks.ts +++ b/apps/mobile/src/utils/hooks.ts @@ -37,7 +37,7 @@ export function useFunctionAfterNavigationTransitionEndWithDelay(fn: () => void, const navigation = useAppStackNavigation() useEffect(() => { - let timeout: NodeJS.Timeout | null = null + let timeout: NodeJS.Timeout | number | null = null const unsubscribe = navigation.addListener('transitionEnd', () => { timeout = setTimeout(fn, timeoutMs) diff --git a/apps/mobile/tsconfig.json b/apps/mobile/tsconfig.json index 94f3a6d681f..9a7067e0a47 100644 --- a/apps/mobile/tsconfig.json +++ b/apps/mobile/tsconfig.json @@ -13,6 +13,12 @@ { "path": "../../packages/uniswap" }, + { + "path": "../../packages/sessions" + }, + { + "path": "../../packages/gating" + }, { "path": "../../packages/api" }, diff --git a/apps/web/.depcheckrc b/apps/web/.depcheckrc index 0469d010fbb..6d78dbec8d8 100644 --- a/apps/web/.depcheckrc +++ b/apps/web/.depcheckrc @@ -83,6 +83,7 @@ ignores: [ 'lib', 'locales', 'nft', + 'notification-service', 'pages', 'polyfills', 'rpc', diff --git a/apps/web/.env.staging b/apps/web/.env.staging index d22934ffff9..0a1782bda6d 100644 --- a/apps/web/.env.staging +++ b/apps/web/.env.staging @@ -1,3 +1,4 @@ # These API keys are intentionally public. Please do not report them - thank you for your concern. REACT_APP_JUPITER_PROXY_URL="https://entry-gateway.backend-prod.api.uniswap.org/jupiter" +ENTRY_GATEWAY_API_URL_OVERRIDE="https://entry-gateway.api.corn-staging.com" diff --git a/apps/web/.gitignore b/apps/web/.gitignore index 9f93a7a3c96..abc356fe394 100644 --- a/apps/web/.gitignore +++ b/apps/web/.gitignore @@ -62,3 +62,5 @@ package-lock.json # Storybook *storybook.log storybook-static/ + +coverage/ diff --git a/apps/web/.storybook/__mocks__/tty.js b/apps/web/.storybook/__mocks__/tty.js new file mode 100644 index 00000000000..60d53bca0ed --- /dev/null +++ b/apps/web/.storybook/__mocks__/tty.js @@ -0,0 +1,10 @@ +// Mock for Node.js tty module to work in browser environment +// Used by @storybook/instrumenter + +module.exports = { + isatty: function () { + return false + }, + ReadStream: function () {}, + WriteStream: function () {}, +} diff --git a/apps/web/.storybook/main.ts b/apps/web/.storybook/main.ts index d5325632a17..a9341f02f1f 100644 --- a/apps/web/.storybook/main.ts +++ b/apps/web/.storybook/main.ts @@ -1,9 +1,8 @@ import type { StorybookConfig } from '@storybook/react-webpack5' import { dirname, join, resolve } from 'path' +import TerserPlugin from 'terser-webpack-plugin' import { DefinePlugin } from 'webpack' -const isDev = process.env.NODE_ENV === 'development' - /** * This function is used to resolve the absolute path of a package. * It is needed in projects that use Yarn PnP or are set up within a monorepo. @@ -40,7 +39,8 @@ const config: StorybookConfig = { config.plugins.push( new DefinePlugin({ - __DEV__: isDev, + __DEV__: process.env.NODE_ENV === 'development', + 'process.env.IS_UNISWAP_EXTENSION': JSON.stringify(process.env.STORYBOOK_EXTENSION || 'false'), }), ) @@ -66,21 +66,81 @@ const config: StorybookConfig = { use: ['@svgr/webpack'], }) + // Add babel-loader for TypeScript/JavaScript transpilation + // @storybook/preset-create-react-app removes TypeScript rules + config?.module?.rules && + config.module.rules.push({ + test: /\.(tsx?|jsx?)$/, + // Exclude node_modules except for expo packages and related modules + exclude: /node_modules\/(?!(expo-.*|@expo|@react-native|@uniswap\/.*)\/).*/, + use: { + loader: 'babel-loader', + options: { + presets: [ + '@babel/preset-env', + ['@babel/preset-react', { runtime: 'automatic' }], + '@babel/preset-typescript', + ], + cacheDirectory: true, + }, + }, + }) + + // Add babel-loader for React Native packages in node_modules config?.module?.rules && config.module.rules.push({ - test: /\.tsx?$/, - use: 'ts-loader', - exclude: /node_modules/, + test: /\.(tsx?|jsx?)$/, + // Exclude node_modules except for expo packages and related modules + exclude: /node_modules\/(?!(expo-.*|@expo|@react-native|@uniswap\/.*)\/).*/, + use: { + loader: 'babel-loader', + options: { + presets: [ + '@babel/preset-env', + ['@babel/preset-react', { runtime: 'automatic' }], + '@babel/preset-typescript', + ], + cacheDirectory: true, + }, + }, + }) + + // Add babel-loader for React Native packages in node_modules + config?.module?.rules && + config.module.rules.push({ + test: /\.(tsx?|jsx?)$/, + include: [ + /node_modules\/react-native-reanimated/, + /node_modules\/react-native-gesture-handler/, + /node_modules\/@react-native/, + /node_modules\/react-native\//, + ], + use: { + loader: 'babel-loader', + options: { + presets: ['@babel/preset-react', '@babel/preset-typescript'], + plugins: [], + cacheDirectory: true, + }, + }, }) config.resolve ??= {} - // Add fallback for Node.js 'os' module + // Configure resolve extensions to prefer .web files + config.resolve.extensions = ['.web.tsx', '.web.ts', '.web.jsx', '.web.js', '.tsx', '.ts', '.jsx', '.js'] + + // Add fallback for Node.js modules not available in browser config.resolve.fallback = { ...(config.resolve.fallback || {}), os: false, + tty: require.resolve('./__mocks__/tty.js'), + fs: false, + path: false, + util: false, } + // Configure webpack aliases for React Native and compatibility config.resolve = { ...config.resolve, alias: { @@ -92,6 +152,109 @@ const config: StorybookConfig = { config.resolve.modules = [resolve(__dirname, '../src'), 'node_modules'] + // Configure optimization - disable minimization in dev to prevent Storybook errors + if (process.env.NODE_ENV === 'production') { + config.optimization = { + ...config.optimization, + minimize: true, + minimizer: [ + new TerserPlugin({ + parallel: 2, // Reduce from default (~8 CPU cores) to prevent memory exhaustion + terserOptions: { + compress: { + drop_console: true, + drop_debugger: true, + pure_funcs: ['console.log', 'console.info'], + }, + mangle: true, + output: { + comments: false, + }, + }, + extractComments: false, + }), + ], + splitChunks: { + chunks: 'all', + cacheGroups: { + vendor: { + test: /[\\/]node_modules[\\/]/, + name: 'vendors', + priority: 10, + }, + tamagui: { + test: /[\\/]node_modules[\\/]tamagui[\\/]/, + name: 'tamagui', + priority: 20, + }, + reactNative: { + test: /[\\/]node_modules[\\/]react-native/, + name: 'react-native', + priority: 20, + }, + }, + maxSize: 500000, // 500KB chunks to reduce memory footprint + }, + } + } else { + // In development, explicitly disable minimization + config.optimization = { + ...config.optimization, + minimize: false, + minimizer: [], + splitChunks: { + chunks: 'all', + cacheGroups: { + vendor: { + test: /[\\/]node_modules[\\/]/, + name: 'vendors', + priority: 10, + }, + tamagui: { + test: /[\\/]node_modules[\\/]tamagui[\\/]/, + name: 'tamagui', + priority: 20, + }, + reactNative: { + test: /[\\/]node_modules[\\/]react-native/, + name: 'react-native', + priority: 20, + }, + }, + maxSize: 500000, + }, + } + } + + // Disable source maps for production Storybook builds to save memory + if (process.env.NODE_ENV === 'production') { + config.devtool = false + } + + // Remove ForkTsCheckerWebpackPlugin - it checks entire app, not just stories + const tsCheckerPlugin = + config.plugins && config.plugins.find((plugin) => plugin?.constructor.name === 'ForkTsCheckerWebpackPlugin') + + if (tsCheckerPlugin) { + config.plugins = config.plugins?.filter((p) => p !== tsCheckerPlugin) + } + + // Enable webpack persistent caching for faster rebuilds + config.cache = { + type: 'filesystem', + cacheDirectory: resolve(__dirname, '../node_modules/.cache/storybook'), + buildDependencies: { + config: [__filename], + }, + } + + // Configure performance hints + config.performance = { + maxAssetSize: 512000 * 2, + maxEntrypointSize: 512000 * 2, + hints: 'warning', + } + return config }, } diff --git a/apps/web/CLAUDE.md b/apps/web/CLAUDE.md index a5256033071..0098f3b1840 100644 --- a/apps/web/CLAUDE.md +++ b/apps/web/CLAUDE.md @@ -22,18 +22,24 @@ bun lint bun lint:fix # Run tests -bun run test # Run all tests +bun run test # Run all tests bun run test:watch # Watch mode bun run test:set1 # Components only bun run test:set2 # Pages and state bun run test:set3 # Hooks, NFT, utils bun run test:set4 # Remaining tests -# Run E2E tests -bun playwright:test - # Build for production bun build:production + +# Run production preview web server +bun preview + +# Run E2E Playwright tests +bun e2e # Run all e2e tests +bun e2e:no-anvil # Run non-anvil e2e tests +bun e2e:anvil # Run anvil e2e tests +bun e2e ExampleTest.e2e.test # Run a specific test file ``` ### Monorepo Commands (from root) diff --git a/apps/web/functions/api/image/pools.tsx b/apps/web/functions/api/image/pools.tsx index 79b1f698a04..c62d4c9a7ff 100644 --- a/apps/web/functions/api/image/pools.tsx +++ b/apps/web/functions/api/image/pools.tsx @@ -1,13 +1,13 @@ // biome-ignore-all lint/correctness/noRestrictedElements: ignoring for the whole file -import { GraphQLApi } from '@universe/api' +import { ProtocolVersion } from '@universe/api/src/clients/graphql/__generated__/schema-types' import { ImageResponse } from '@vercel/og' import { WATERMARK_URL } from 'functions/constants' import getFont from 'functions/utils/getFont' import getNetworkLogoUrl from 'functions/utils/getNetworkLogoURL' import getPool from 'functions/utils/getPool' import { getRequest } from 'functions/utils/getRequest' -import { Context } from 'hono' +import { type Context } from 'hono' function UnknownTokenImage({ symbol }: { symbol?: string }) { const ticker = symbol?.slice(0, 3) @@ -174,7 +174,7 @@ export async function poolImageHandler(c: Context) { > {data.name}
- {data.poolData?.protocolVersion === GraphQLApi.ProtocolVersion.V2 && ( + {data.poolData?.protocolVersion === ProtocolVersion.V2 && (
(type) => type; } } - + - + - + - + diff --git a/apps/web/functions/explore/pools/pool.test.ts b/apps/web/functions/explore/pools/pool.test.ts index 803b6369f2f..7511682a029 100644 --- a/apps/web/functions/explore/pools/pool.test.ts +++ b/apps/web/functions/explore/pools/pool.test.ts @@ -14,7 +14,7 @@ const pools = [ { address: '0xD1F1baD4c9E6c44DeC1e9bF3B94902205c5Cd6C3', network: 'optimism', - name: 'USDC/WLD', + name: 'USDC.e/WLD', image: 'http://localhost:3000/api/image/pools/optimism/0xD1F1baD4c9E6c44DeC1e9bF3B94902205c5Cd6C3', }, ] diff --git a/apps/web/playwright.config.ts b/apps/web/playwright.config.ts index 26e615d9c3f..ece5a5baaa5 100644 --- a/apps/web/playwright.config.ts +++ b/apps/web/playwright.config.ts @@ -1,13 +1,13 @@ import { defineConfig, devices } from '@playwright/test' -import dotenv from 'dotenv' +import { config } from 'dotenv' import ms from 'ms' import path from 'path' -const IS_CI = process.env.CI === 'true' +// Load environment variables from .env file +// This ensures the VSCode Playwright extension has access to env vars +config({ path: path.resolve(__dirname, '.env') }) -if (!IS_CI) { - dotenv.config({ path: path.resolve(__dirname, '.env.local') }) -} +const IS_CI = process.env.CI === 'true' // Handle asset files and platform-specific imports for Node.js // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -51,15 +51,16 @@ export default defineConfig({ testMatch: '**/*.e2e.test.ts', globalTeardown: './src/playwright/anvil/global-teardown.ts', workers: 1, // this is manually configured in the github action depending on type of tests - fullyParallel: true, + fullyParallel: false, maxFailures: IS_CI ? 10 : undefined, retries: IS_CI ? 3 : 0, reporter: IS_CI && process.env.REPORT_TO_SLACK ? [['blob'], ['list']] : 'list', - timeout: ms('60s'), + timeout: ms('120s'), expect: { - timeout: ms('10s'), + timeout: ms('15s'), }, use: { + actionTimeout: ms('30s'), screenshot: 'off', video: 'retain-on-failure', trace: 'retain-on-failure', diff --git a/apps/web/public/app-sitemap.xml b/apps/web/public/app-sitemap.xml index bc83f2fc08c..06bb6000f83 100644 --- a/apps/web/public/app-sitemap.xml +++ b/apps/web/public/app-sitemap.xml @@ -126,4 +126,16 @@ weekly 0.5 + + https://app.uniswap.org/positions/create + 2024-09-17T19:57:27.976Z + weekly + 0.7 + + + https://app.uniswap.org/positions + 2024-09-17T19:57:27.976Z + weekly + 0.7 + diff --git a/apps/web/public/csp.json b/apps/web/public/csp.json index bd7590b734f..53f051b7aac 100644 --- a/apps/web/public/csp.json +++ b/apps/web/public/csp.json @@ -1,6 +1,6 @@ { "defaultSrc": ["'self'"], - "scriptSrc": ["'self'", "'wasm-unsafe-eval'"], + "scriptSrc": ["'self'", "'wasm-unsafe-eval'", "https://challenges.cloudflare.com"], "styleSrc": [ "'self'", "'unsafe-inline'", @@ -20,7 +20,8 @@ "https://buy.moonpay.com/", "https://verify.walletconnect.com/", "https://verify.walletconnect.org/", - "https://id.porto.sh" + "https://id.porto.sh", + "https://wrapped.uniswap.org" ], "mediaSrc": ["self", "*"], "formAction": ["'none'"], diff --git a/apps/web/public/images/notifications/monad_banner_light.png b/apps/web/public/images/notifications/monad_banner_light.png new file mode 100644 index 0000000000000000000000000000000000000000..7d6af153b91cf418e898eeb6ddaf1c6a13a08be5 GIT binary patch literal 29178 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 V2#N)vC4m3{002ovPDHLkV1f~OZ^{4w literal 0 HcmV?d00001 diff --git a/apps/web/public/images/notifications/monad_logo_filled.png b/apps/web/public/images/notifications/monad_logo_filled.png new file mode 100644 index 0000000000000000000000000000000000000000..9e839715080082419fd27a925867d80353bce24a GIT binary patch literal 2017 zcmV<72Oju|P)i#DH0qK9~H)FOlOWJii;G9k}5<=BwFy%M2dvrj60g#;dDU?5+Myz zkU~L}hJy}Qj$Hwkaz;WT4LK?k`*t7m-(!tA@A-CSXJ^m&ev)O*cl$B_ee>qcdvk*z zLuN{AMd5j)EX4^yG|EiF>d(WpJ!p^+bt0}4)88azZK^U*BLjrahsz}%&iTzrBELl2 zN32*VA?ixhT26S=KU8+tHIC~Jgp<5>Q81Ti2OxyF7FWow0?wRWRTjr(ddDoT8?Z>9 z#q@>Q!E0SU*O^O`t3U1(tLp>{QMphZJlE;{NC5+1$7{EUxk!D`r{_H}aI#zxjrO-> z)aR9Wcl+XPfO#}9s0cO*h;EAmsL$Idg9@~1h)aJ>sF@h`wmL_;KP=OdG?g)$&ybmSEy&m`KySJ%lX%@i_`<`SE3ymku)+Ul!%){ z)P*^p?%!!r<2m&LYKBH-fvgJo{$0wD;k`uT6L}?^jP?1R5mNdm^GduTWyn}j()cI2 zNQ>(Pgx+^>faYpG&E4}TD}*OT^HR_;KLZB{tNZk;e9&#pju-Ut=Q-8o1>L@pjWzm$ zijr9r$T1M@^!EeC; zK6E+wyR^W$aSewI=g9$x>%Kglchb$qPUM0Z^4HllCvjuyEy7^o+D9P(hmXaz(Ln)q9fuLrw39QSgd#9q~bxi>$A<^=h33S zNj%Va&Pt4C@H;N>!-xcIV^u)JV5S4cM&a=9Za}=-H}vMghy-lMiPjzo{1Rd?T|yz^ zh}I5{xqrvgnt|XR(T#DO4Xg-=Cu=gpKqKbvMR(KK=lIj3{fC5BoQZo~e#}M>t)0YO zxRq7W6Aw53)AU-<_$1sC4mP$X7$9}4c>#BXoJSU{M6+>4`OS1&iM_?%8)V9*_vK`SUH zV~hnv4I@t1HHq0w!2{BLNa)IFiz!9|;)%sA0AZwcA>Nzo>03G4Vwx>7I!fn>y>MKs zFtI{#;hl~2Ao`n_agiy!~nPAi+ zbmLyP!r|A&r#PT3rn4pE7MbMoTv?$`RS?3NS|2cOO>kj@h#$$c>wN~65CE~>- zbM-{#fE=7|#E*t0&B1jdT=^AOJluQI1)7QblSw)~X%4O(oGVz~rcswMsp>_MSU}v! z&8zx1Vk4d2%H`#Z7HdL08)-Hx9z85C>X5WX8?JKIU0)4wueP$|*6P9-$zTyK*31&$;K@&LNNF_{FsV|xvuS^mi3_Q=GCT3IK&t6 z&0?hJ_l_J*E*uDJGqw>kXaBdJ^Jy(9)2kP^;!1CQ30vWiq{ByrmBr|37Pe9m5SWOV z25DCXQ=3=0EN5nU52;Q}vlI~nhKp;bKC^GFN?6kxSdxNBz}16ul3zli^5@IVAO=gD zaPg9hdI*4k4RQoxFk1^7qrkb-TD;VOMqUVRFbij2_rfe-d5MFxmaP#Q)`t0bnv0hr z5mc9g#5_4B^ieh%jB}*D@~acfye!%0dz1li@WFb)-_5cjusR%jJ;P85#+T^cypv#RRnB4rFL>W|;B3>d5K#rirNtcF;Q_{LJE2vNhYKtN9pR&xGo<^qO5 zDAb-GE>h2e9WL=Z=qs&#ptwqXVu39}!|g%WosB1VmC0dHyq`{MPxa%Z^OzA0^F05~(b+TQ}S9UoXvoBSb7L&B2;p)_tp^jTYZ#j3ufh_&+gvvV5&I5z$xvj(lF}H%p z4wqC`dVfPZ0MSJSHcZd;s_UFoQ9bxtrW?ZMQoqP zBnm1wR-kS4-(e`X)KHC|M>-D~sj15D8X52kP#T;Ugg6wa00000NkvXXu0mjfMtayE literal 0 HcmV?d00001 diff --git a/apps/web/public/nfts-sitemap.xml b/apps/web/public/nfts-sitemap.xml index 384bc3a2dfd..ad5990a816c 100644 --- a/apps/web/public/nfts-sitemap.xml +++ b/apps/web/public/nfts-sitemap.xml @@ -2,707 +2,667 @@ https://app.uniswap.org/nfts/collection/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x60e4d786628fea6478f785a6d7e704777c86a7c6 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0xed5af388653567af2f388e6224dc7c4b3241c544 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x34d85c9cdeb23fa97cb08333b511ac86e1c4e258 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x99a9b7c1116f9ceeb1652de04d5969cce509b069 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x49cf6f5d44e70224e2e23fdcdd2c053f30ada28b - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0xb7f7f6c52f2e2fdb1963eab30438024864c313f6 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x23581767a106ae21c074b2276d25e5c3e136a68b - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x8a90cab2b38dba80c64b7734e58ee1db38b8992e - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0xba30e5f9bb24caa003e9f2f0497ad287fdf95623 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0xbd3531da5cf5857e7cfaa92426877b022e612cf8 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x7bd29408f11d2bfc23c34f18275bbf23bb716bc7 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x306b1ea3ecdf94ab739f1910bbda052ed4a9f949 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x1a92f7381b9f03921564a437210bb9396471050c - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x5cc5b05a8a13e3fbdb0bb9fccd98d38e50f90c38 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x5af0d9827e0c53e4799bb226655a1de152a425a5 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x3bf2922f4520a8ba0c2efc3d2a1539678dad5e9d - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0xe785e82358879f061bc3dcac6f0444462d4b5330 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x76be3b62873462d2142405439777e971754e8e77 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0xfd43af6d3fe1b916c026f6ac35b3ede068d1ca01 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x1cb1a5e65610aeff2551a50f76a87a7d3fb649c6 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0xff9c1b15b16263c61d017ee9f65c50e4ae0113d7 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x6339e5e072086621540d0362c4e3cea0d643e114 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0xb932a70a57673d89f4acffbe830e8ed7f75fb9e0 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x79fcdef22feed20eddacbb2587640e45491b757f - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0xa3aee8bce55beea1951ef834b99f3ac60d1abeeb - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x769272677fab02575e84945f03eca517acc544cc - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x4db1f25d3d98600140dfc18deb7515be5bd293af - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x34eebee6942d8def3c125458d1a86e0a897fd6f9 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x59468516a8259058bad1ca5f8f4bff190d30e066 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x394e3d3044fc89fcdd966d3cb35ac0b32b0cda91 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x60bb1e2aa1c9acafb4d34f71585d7e959f387769 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x28472a58a490c5e09a238847f66a68a47cc76f0f - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x341a1c534248966c4b6afad165b98daed4b964ef - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x82c7a8f707110f5fbb16184a5933e9f78a34c6ab - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0xccc441ac31f02cd96c153db6fd5fe0a2f4e6a68d - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x764aeebcf425d56800ef2c84f2578689415a2daa - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x160c404b2b49cbc3240055ceaee026df1e8497a0 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0xd2f668a8461d6761115daf8aeb3cdf5f40c532c6 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x39ee2c7b3cb80254225884ca001f57118c8f21b6 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0xd774557b647330c91bf44cfeab205095f7e6c367 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x1792a96e5668ad7c167ab804a100ce42395ce54d - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0xf87e31492faf9a91b02ee0deaad50d51d56d5d4d - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x04afa589e2b933f9463c5639f412b183ec062505 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0xe75512aa3bec8f00434bbd6ad8b0a3fbff100ad6 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x348fc118bcc65a92dc033a951af153d14d945312 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x892848074ddea461a15f337250da3ce55580ca85 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x5946aeaab44e65eb370ffaa6a7ef2218cff9b47d - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x282bdd42f4eb70e7a9d9f40c8fea0825b7f68c5d - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x4b15a9c28034dc83db40cd810001427d3bd7163d - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x7ea3cca10668b8346aec0bf1844a49e995527c8b - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0xb852c6b5892256c264cc2c888ea462189154d8d7 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x9378368ba6b85c1fba5b131b530f5f5bedf21a18 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x2acab3dea77832c09420663b0e1cb386031ba17b - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x0c2e57efddba8c768147d1fdf9176a0a6ebd5d83 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x08d7c0242953446436f34b4c78fe9da38c73668d - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x8943c7bac1914c9a7aba750bf2b6b09fd21037e0 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x364c828ee171616a39897688a831c2499ad972ec - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x7f36182dee28c45de6072a34d29855bae76dbe2f - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0xf61f24c2d93bf2de187546b14425bf631f28d6dc - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x797a48c46be32aafcedcfd3d8992493d8a1f256b - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x123b30e25973fecd8354dd5f41cc45a3065ef88c - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x6632a9d63e142f17a668064d41a21193b49b41a0 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0xf4ee95274741437636e748ddac70818b4ed7d043 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x57a204aa1042f6e66dd7730813f4024114d74f37 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0xd1258db6ac08eb0e625b75b371c023da478e94a9 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x75e95ba5997eb235f40ecf8347cdb11f18ff640b - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0xd532b88607b1877fe20c181cba2550e3bbd6b31c - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0xa1d4657e0e6507d5a94d06da93e94dc7c8c44b51 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0xedb61f74b0d09b2558f1eeb79b247c1f363ae452 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x7d8820fa92eb1584636f4f5b8515b5476b75171a - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x231d3559aa848bf10366fb9868590f01d34bf240 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0xad9fd7cb4fc7a0fbce08d64068f60cbde22ed34c - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x0e9d6552b85be180d941f1ca73ae3e318d2d4f1f - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0xb716600ed99b4710152582a124c697a7fe78adbf - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0xaadc2d4261199ce24a4b0a57370c4fcf43bb60aa - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x4e1f41613c9084fdb9e34e11fae9412427480e56 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x79986af15539de2db9a5086382daeda917a9cf0c - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0xc99c679c50033bbc5321eb88752e89a93e9e83c5 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0xc36cf0cfcb5d905b8b513860db0cfe63f6cf9f5c - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x9c8ff314c9bc7f6e59a9d9225fb22946427edc03 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x3110ef5f612208724ca51f5761a69081809f03b7 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x036721e5a769cc48b3189efbb9cce4471e8a48b1 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x524cab2ec69124574082676e6f654a18df49a048 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x7ab2352b1d2e185560494d5e577f9d3c238b78c5 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x32973908faee0bf825a343000fe412ebe56f802a - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x7daec605e9e2a1717326eedfd660601e2753a057 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0xc1caf0c19a8ac28c41fe59ba6c754e4b9bd54de9 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x33fd426905f149f8376e227d0c9d3340aad17af1 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x466cfcd0525189b573e794f554b8a751279213ac - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x6be69b2a9b153737887cfcdca7781ed1511c7e36 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x80336ad7a747236ef41f47ed2c7641828a480baa - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x9401518f4ebba857baa879d9f76e1cc8b31ed197 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x4b61413d4392c806e6d0ff5ee91e6073c21d6430 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0xc3f733ca98e0dad0386979eb96fb1722a1a05e69 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x09233d553058c2f42ba751c87816a8e9fae7ef10 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x960b7a6bcd451c9968473f7bbfd9be826efd549a - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x36d30b3b85255473d27dd0f7fd8f35e36a9d6f06 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x698fbaaca64944376e2cdc4cad86eaa91362cf54 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x497a9a79e82e6fc0ff10a16f6f75e6fcd5ae65a8 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x41a322b28d0ff354040e2cbc676f0320d8c8850d - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0xa9c0a07a7cb84ad1f2ffab06de3e55aab7d523e8 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x942bc2d3e7a589fe5bd4a5c6ef9727dfd82f5c8a - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x8821bee2ba0df28761afff119d66390d594cd280 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x8c6def540b83471664edc6d5cf75883986932674 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x8d9710f0e193d3f95c0723eaaf1a81030dc9116d - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x86825dfca7a6224cfbd2da48e85df2fc3aa7c4b1 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x629a673a8242c2ac4b7b8c5d8735fbeac21a6205 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x9a534628b4062e123ce7ee2222ec20b86e16ca8f - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0xc2c747e0f7004f9e8817db2ca4997657a7746928 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x73da73ef3a6982109c4d5bdb0db9dd3e3783f313 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0xc92ceddfb8dd984a89fb494c376f9a48b999aafc - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x3248e8ba90facc4fdd3814518c14f8cc4d980e4b - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x67d9417c9c3c250f61a83c7e8658dac487b56b09 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0xb6a37b5d14d502c3ab0ae6f3a0e058bc9517786e - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x86c10d10eca1fca9daf87a279abccabe0063f247 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x4b3406a41399c7fd2ba65cbc93697ad9e7ea61e5 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0xb0640e8b5f24bedc63c33d371923d68fde020303 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0xd3d9ddd0cf0a5f0bfb8f7fceae075df687eaebab - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0xa5c0bd78d1667c13bfb403e2a3336871396713c5 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x4d7d2e237d64d1484660b55c0a4cc092fa5e6716 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0xfcb1315c4273954f74cb16d5b663dbf479eec62e - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x66d1db16101502ed0ca428842c619ca7b62c8fef - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x128675d4fddbc4a0d3f8aa777d8ee0fb8b427c2f - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x19b86299c21505cdf59ce63740b240a9c822b5e4 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0xacf63e56fd08970b43401492a02f6f38b6635c91 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x0bebad1ff25c623dff9605dad4a8f782d5da37df - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0xdceaf1652a131f32a821468dc03a92df0edd86ea - 2025-04-11T20:35:02.652Z - 0.7 - - - https://app.uniswap.org/nfts/collection/0x273f7f8e6489682df756151f5525576e322d51a3 - 2025-04-11T20:35:02.652Z - 0.7 - - - https://app.uniswap.org/nfts/collection/0x77372a4cc66063575b05b44481f059be356964a4 - 2025-04-11T20:35:02.652Z - 0.7 - - - https://app.uniswap.org/nfts/collection/0xf5b0a3efb8e8e4c201e2a935f110eaaf3ffecb8d - 2025-04-11T20:35:02.652Z - 0.7 - - - https://app.uniswap.org/nfts/collection/0x22c36bfdcef207f9c0cc941936eff94d4246d14a - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x59325733eb952a92e069c87f0a6168b29e80627f - 2025-04-11T20:35:02.652Z - 0.7 - - - https://app.uniswap.org/nfts/collection/0x0e3a2a1f2146d86a604adc220b4967a898d7fe07 - 2025-04-11T20:35:02.652Z - 0.7 - - - https://app.uniswap.org/nfts/collection/0x3af2a97414d1101e2107a70e7f33955da1346305 - 2025-04-11T20:35:02.652Z - 0.7 - - - https://app.uniswap.org/nfts/collection/0x5ab21ec0bfa0b29545230395e3adaca7d552c948 - 2025-04-11T20:35:02.652Z - 0.7 - - - https://app.uniswap.org/nfts/collection/0x617913dd43dbdf4236b85ec7bdf9adfd7e35b340 - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 - https://app.uniswap.org/nfts/collection/0x3fe1a4c1481c8351e91b64d5c398b159de07cbc5 - 2025-04-11T20:35:02.652Z + https://app.uniswap.org/nfts/collection/0xd4e4078ca3495de5b1d4db434bebc5a986197782 + 2025-03-20T21:18:53.078Z 0.7 - https://app.uniswap.org/nfts/collection/0xd4e4078ca3495de5b1d4db434bebc5a986197782 - 2025-04-11T20:35:02.652Z + https://app.uniswap.org/nfts/collection/0x77372a4cc66063575b05b44481f059be356964a4 + 2025-03-20T21:18:53.078Z 0.7 https://app.uniswap.org/nfts/collection/0x062e691c2054de82f28008a8ccc6d7a1c8ce060d - 2025-04-11T20:35:02.652Z + 2025-03-20T21:18:53.078Z 0.7 \ No newline at end of file diff --git a/apps/web/public/pools-sitemap.xml b/apps/web/public/pools-sitemap.xml index 03d6f39b07c..279edd061cb 100644 --- a/apps/web/public/pools-sitemap.xml +++ b/apps/web/public/pools-sitemap.xml @@ -2,13132 +2,8147 @@ https://app.uniswap.org/explore/pools/ethereum/0xcbcdf9626bc03e24f779434178a73a0b4bad62ed - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x4e68ccd3e89f51c3074ca5072bbac773960dfa36 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x4585fe77225b41b697c938b018e2ac67ac5a20c0 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xc63b0708e2f7e69cb8a1df0e1389a98c35a76d52 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x99ac8ca7087fa4a2a1fb6357269965a2014abc35 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x11b815efb8f581194ae79006d24e0d814b7697f6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xa6cc3c2531fdaa6ae1a3ca84c2855806728693e8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x5777d92f208679db4b9778590fa3cab3ac9e2168 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x1d42064fc4beb5f8aaf85f4617ae8b3b5b8bd801 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xc2e9f25be6257c210d7adf0d4cd6e3e881ba25f8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x11950d141ecb863f01007add7d1a342041227b58 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xc5c134a1f112efa96003f8559dba6fac0ba77692 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x109830a1aaad605bbf02a9dfa7b0b92ec2fb7daa - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x1df4c6e36d61416813b42fe32724ef11e363eddc - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x12d6867fa648d269835cf69b49f125147754b54d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x3416cf6c708da44db2624d63ea0aaef7113527c6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xe8c6c9227491c0a8156a0106a0204d881bb7e531 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x04708077eca6bb527a5bbbd6358ffb043a9c1c14 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x9db9e0e53058c89e5b94e29621a205198648425b - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xf239009a101b6b930a527deaab6961b6e7dec8a6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xfe0df74636bc25c7f2400f22fe7dae32d39443d2 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xf4c5e0f4590b6679b3030d29a84857f226087fef - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x5764a6f2212d502bc5970f9f129ffcd61e5d7563 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xa3f558aebaecaf0e11ca4b2199cc5ed341edfd74 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x99132b53ab44694eeb372e87bced3929e4ab8456 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x6c6bc977e13df9b0de53b251522280bb72383700 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x9d96880952b4c80a55099b9c258250f2cc5813ec - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x3afdc5e6dfc0b0a507a8e023c9dce2cafc310316 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x290a6a7460b308ee3f19023d2d00de604bcf5b42 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xac4b3dacb91461209ae9d41ec517c2b9cb1b7daf - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x60594a405d53811d3bc4766596efd80fd545a270 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x331399c614ca67dee86733e5a2fba40dbb16827c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x4b5ab61593a2401b1075b90c04cbcdd3f87ce011 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x844eb5c280f38c7462316aad3f338ef9bda62668 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xe936f0073549ad8b1fa53583600d629ba9375161 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x2f62f2b4c5fcd7570a709dec05d68ea19c82a9ec - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x381fe4eb128db1621647ca00965da3f9e09f4fac - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x97e7d56a0408570ba1a7852de36350f7713906ec - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xcd423f3ab39a11ff1d9208b7d37df56e902c932b - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xe15e6583425700993bd08f51bf6e7b73cd5da91b - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x69d91b94f0aaf8e8a2586909fa77a5c2c89818d5 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xe42318ea3b998e8355a3da364eb9d48ec725eb45 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xad9ef19e289dcbc9ab27b83d2df53cdeff60f02d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x3b685307c8611afb2a9e83ebc8743dc20480716e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x7bea39867e4169dbe237d55c8242a8f2fcdcc387 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x7b1e5d984a43ee732de195628d20d05cfabc3cc7 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x7858e59e0c01ea06df3af3d20ac7b0003275d4bf - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xae2a25cbdb19d0dc0dddd1d2f6b08a6e48c4a9a9 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x21b8065d10f73ee2e260e5b47d3344d3ced7596e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x0d4a11d5eeaac28ec3f61d100daf4d40471f1852 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x517f9dd285e75b599234f7221227339478d0fcc8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xa43fe16908251ee70ef74718545e4fe6c5ccec9f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x0af81cd5d9c124b4859d65697a4cd10ee223746a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xca7c2771d248dcbe09eabe0ce57a62e18da178c0 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x09d1d767edf8fa23a64c51fa559e0688e526812f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x7b73644935b8e68019ac6356c40661e1bc315860 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x180efc1349a69390ade25667487a826164c9c6e4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x9c4fe5ffd9a9fc5678cfbd93aa2d4fd684b67c4c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xa478c2975ab1ea89e8196811f51a7b7ade33eb11 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xbb2b8038a1640196fbe3e38816f3e67cba72d940 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x9ec9367b8c4dd45ec8e7b800b1f719251053ad60 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xc91ef786fbf6d62858262c82c63de45085dea659 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x197d7010147df7b99e9025c724f13723b29313f8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x25647e01bd0967c1b9599fa3521939871d1d0888 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x2f0b1417aa42ebf0b4ca1154212847f6094d708d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x6ada49aeccf6e556bb7a35ef0119cc8ca795294a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x2a6c340bcbb0a79d3deecd3bc5cbc2605ea9259f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xda2d09fbbf8ee4b5051a0e9b562c5fcb4b393b18 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x48d20b3e529fb3dd7d91293f80638df582ab2daa - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x4028daac072e492d34a3afdbef0ba7e35d8b55c4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xc2eab7d33d3cb97692ecb231a5d0e4a649cb539d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xc5be99a02c6857f9eac67bbce58df5572498f40c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xe4b8583ccb95b25737c016ac88e539d0605949e8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x8dbee21e8586ee356130074aaa789c33159921ca - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x43de4318b6eb91a7cf37975dbb574396a7b5b5c6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x9ff68f61ca5eb0c6606dc517a9d44001e564bb66 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xa29fe6ef9592b5d408cca961d0fb9b1faf497d6d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x1b1137dd16faa651e38a9dfb5d9ffff7767fdf62 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x470e8de2ebaef52014a47cb5e6af86884947f08c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x8fb8e9921922d2ffb529a95d28a0d06d275d7a59 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xd3d2e2692501a5c9ca623199d38826e513033a17 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x97e1fcb93ae7267dbafad23f7b9afaa08264cfd8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xa5e9c917b4b821e4e0a5bbefce078ab6540d6b5e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x2cc846fff0b08fb3bffad71f53a60b4b6e6d6482 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x959873fb4fc11825fba83c80c4c632db1e936e15 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xa7480aafa8ad2af3ce24ac6853f960ae6ac7f0c4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xc7e6b676bfc73ae40bcc4577f22aab1682c691c6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x570febdf89c07f256c75686caca215289bb11cfc - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x343fd171caf4f0287ae6b87d75a8964dc44516ab - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xcaa004418eb42cdf00cb057b7c9e28f0ffd840a5 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xe3d3551bb608e7665472180a20280630d9e938aa - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xb6b0c651c37ec4ca81c0a128420e02001a57fac2 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x4e34da137f0b317c633838458e0c923a5e088752 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xfe9e7931e55c514c33d489c88582fa36e84bd8e3 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x5281e311734869c64ca60ef047fd87759397efe6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x149148acc3b06b8cc73af3a10e84189243a35925 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x8ef79d6c328c25da633559c20c75f638a4863462 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x14af1804dbbf7d621ecc2901eef292a24a0260ea - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x80a9ae39310abf666a87c743d6ebbd0e8c42158e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xc31e54c7a869b9fcbecc14363cf510d1c41fa443 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x2f5e87c9312fa29aed5c179e456625d79015299c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xc6962004f452be9203591991d15f6b388e09e8d0 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xc6f780497a95e246eb9449f5e4770916dcd6396a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x641c00a822e8b671738d32a431a4fb6074e5c79d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x92c63d0e701caae670c9415d91c474f686298f00 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x1aeedd3727a6431b8f070c0afaa81cc74f273882 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xcda53b1f66614552f834ceef361a8d12a0b8dad8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x35218a1cbac5bbc3e57fd9bd38219d37571b3537 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x17c14d2c404d167802b16c450d3c99f88f2c4f4d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x468b88941e7cc0b88c1869d68ab6b570bcef62ff - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xdbaeb7f0dfe3a0aafd798ccecb5b22e708f7852c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x149e36e72726e0bcea5c59d40df2c43f60f5a22d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xbaaf1fc002e31cb12b99e4119e5e350911ec575b - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xa67f72f21bd9f91db2da2d260590da5e6c437009 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x92fd143a8fa0c84e016c2765648b9733b0aa519e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x7cf803e8d82a50504180f417b8bc7a493c0a0503 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x81c48d31365e6b526f6bbadc5c9aafd822134863 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x446bf9748b4ea044dd759d9b9311c70491df8f29 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xc82819f72a9e77e2c0c3a69b3196478f44303cf4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x50c7390dfdd3756139e6efb5a461c2eb7331ceb4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x1dfc1054e0e2a10e33c9ca21aad5aa8a1cce91e3 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xc91b7b39bbb2c733f0e7459348fd0c80259c8471 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x59d72ddb29da32847a4665d08ffc8464a7185fae - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x09ba302a3f5ad2bf8853266e271b005a5b3716fe - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xa77d77c9773c35e910acc2e30cefe52b54a58414 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x8da66e470403b3d3eee66c67e2c61fda6e248ad1 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x2f020e708811c054f146eebcc4d5a215fd4eec26 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x7e7fb3cceca5f2ac952edf221fd2a9f62e411980 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x68c685fd52a56f04665b491d491355a624540e85 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xa8328bf492ba1b77ad6381b3f7567d942b000baf - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xc0cf0f380ddb44dbcaf19a86d094c8bba3efa04a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xa169d1ab5c948555954d38700a6cdaa7a4e0c3a0 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x1862200e8e7ce1c0827b792d0f9546156f44f892 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x05bbaaa020ff6bea107a9a1e06d2feb7bfd79ed2 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xd02a4969dc12bb889754361f8bcf3385ac1b2077 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xc24f7d8e51a64dc1238880bd00bb961d54cbeb29 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x7c06736e41236fecd681dd3353aa77ecd19ea565 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xc473e2aee3441bf9240be85eb122abb059a3b57c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x14353445c8329df76e6f15e9ead18fa2d45a8bb6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x2039f8c9cd32ba9cd2ea7e575d5b1abea93f7527 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xd3e11119d2680c963f1cdcffece0c4ade823fb58 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x8e295789c9465487074a65b1ae9ce0351172393f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x97bca422ec0ee4851f2110ea743c1cd0a14835a1 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xbe3ad6a5669dc0b8b12febc03608860c31e2eef6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x56ebd63a756b94d3de9cea194896b4920b64fb01 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xe2ddd33585b441b9245085588169f35108f85a6e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x84436a2af97f37018db116ae8e1b691666db3d00 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x21b8065d10f73ee2e260e5b47d3344d3ced7596e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x0d4a11d5eeaac28ec3f61d100daf4d40471f1852 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x517f9dd285e75b599234f7221227339478d0fcc8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xa43fe16908251ee70ef74718545e4fe6c5ccec9f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x0af81cd5d9c124b4859d65697a4cd10ee223746a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xca7c2771d248dcbe09eabe0ce57a62e18da178c0 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x09d1d767edf8fa23a64c51fa559e0688e526812f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x7b73644935b8e68019ac6356c40661e1bc315860 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x180efc1349a69390ade25667487a826164c9c6e4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x9c4fe5ffd9a9fc5678cfbd93aa2d4fd684b67c4c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xa478c2975ab1ea89e8196811f51a7b7ade33eb11 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xbb2b8038a1640196fbe3e38816f3e67cba72d940 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x9ec9367b8c4dd45ec8e7b800b1f719251053ad60 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xc91ef786fbf6d62858262c82c63de45085dea659 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x197d7010147df7b99e9025c724f13723b29313f8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x25647e01bd0967c1b9599fa3521939871d1d0888 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x2f0b1417aa42ebf0b4ca1154212847f6094d708d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x6ada49aeccf6e556bb7a35ef0119cc8ca795294a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x2a6c340bcbb0a79d3deecd3bc5cbc2605ea9259f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xda2d09fbbf8ee4b5051a0e9b562c5fcb4b393b18 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x48d20b3e529fb3dd7d91293f80638df582ab2daa - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x4028daac072e492d34a3afdbef0ba7e35d8b55c4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xc2eab7d33d3cb97692ecb231a5d0e4a649cb539d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xc5be99a02c6857f9eac67bbce58df5572498f40c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xe4b8583ccb95b25737c016ac88e539d0605949e8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x8dbee21e8586ee356130074aaa789c33159921ca - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x43de4318b6eb91a7cf37975dbb574396a7b5b5c6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x9ff68f61ca5eb0c6606dc517a9d44001e564bb66 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xa29fe6ef9592b5d408cca961d0fb9b1faf497d6d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x1b1137dd16faa651e38a9dfb5d9ffff7767fdf62 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x470e8de2ebaef52014a47cb5e6af86884947f08c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x8fb8e9921922d2ffb529a95d28a0d06d275d7a59 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xd3d2e2692501a5c9ca623199d38826e513033a17 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x97e1fcb93ae7267dbafad23f7b9afaa08264cfd8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xa5e9c917b4b821e4e0a5bbefce078ab6540d6b5e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x2cc846fff0b08fb3bffad71f53a60b4b6e6d6482 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x959873fb4fc11825fba83c80c4c632db1e936e15 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xa7480aafa8ad2af3ce24ac6853f960ae6ac7f0c4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xc7e6b676bfc73ae40bcc4577f22aab1682c691c6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x570febdf89c07f256c75686caca215289bb11cfc - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x343fd171caf4f0287ae6b87d75a8964dc44516ab - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xcaa004418eb42cdf00cb057b7c9e28f0ffd840a5 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xe3d3551bb608e7665472180a20280630d9e938aa - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xb6b0c651c37ec4ca81c0a128420e02001a57fac2 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x4e34da137f0b317c633838458e0c923a5e088752 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xfe9e7931e55c514c33d489c88582fa36e84bd8e3 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x5281e311734869c64ca60ef047fd87759397efe6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x149148acc3b06b8cc73af3a10e84189243a35925 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x8ef79d6c328c25da633559c20c75f638a4863462 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x68f5c0a2de713a54991e01858fd27a3832401849 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x4533bad2dc588f0fadf8d2e72386d4cd6a19b519 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x85149247691df622eaf1a8bd0cafd40bc45154a9 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x0392b358ce4547601befa962680bede836606ae2 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x1c3140ab59d6caf9fa7459c6f83d4b52ba881d36 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xd1f1bad4c9e6c44dec1e9bf3b94902205c5cd6c3 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x03af20bdaaffb4cc0a521796a223f7d85e2aac31 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x73b14a78a0d396c521f954532d43fd5ffe385216 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xac85eaf55e9c60ed40a683de7e549d23fdfbeb33 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x04f6c85a1b00f6d9b75f91fd23835974cc07e65c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x730691cdac3cbd4d41fc5eb9d8abbb0cea795b94 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x535541f1aa08416e69dc4d610131099fa2ae7222 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xfc1f3296458f9b2a27a0b91dd7681c4020e09d05 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x85c31ffa3706d1cce9d525a00f1c7d4a2911754c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xd52533a3309b393afebe3176620e8ccfb6159f8a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xff7fbdf7832ae524deda39ca402e03d92adff7a5 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xb589969d38ce76d3d7aa319de7133bc9755fd840 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xf334f6104a179207ddacfb41fa3567feea8595c2 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x1fb3cf6e48f1e7b10213e7b6d87d4c073c7fdb7b - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xd4344ea0c5ade7e22b9b275f0bde7a145dec5a23 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x5b42a63d6741416ce9a7b9f4f16d8c9231ccddd4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x252cbdff917169775be2b552ec9f6781af95e7f6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x2ab22ac86b25bd448a4d9dc041bd2384655299c4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xc858a329bf053be78d6239c4a4343b8fbd21472b - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xa73c628eaf6e283e26a7b1f8001cf186aa4c0e8e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xb533c12fb4e7b53b5524eab9b47d93ff6c7a456f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x2ae3d6096d8215ac2acddf30c60caa984ea5debe - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x19ea026886cbb7a900ecb2458636d72b5cae223b - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x6f32061f59a21086c334d0d45f804089ce374aaf - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xfaf037caafa9620bfaebc04c298bf4a104963613 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xadb35413ec50e0afe41039eac8b930d313e94fa4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xe9e3893921de87b1194a8108f9d70c24bde71c27 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xf1f199342687a7d78bcc16fce79fa2665ef870e1 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xf44acaa38be5e965c5ddf374e7a2ba270e580684 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x36e42931a765022790b797963e42c5522d6b585a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x5adba6c5589c50791dd65131df29677595c7efa7 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x3249e3e3e4133ee18e65347daf586610cc265f54 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xca1b837c87c6563910c2befa48834fa2a8c3d72d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x6ef7b14bcd8d989cef8f8ec8ba4bf371b2ac95fd - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x37ffd11972128fd624337ebceb167c8c0a5115ff - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xe62bd99a9501ca33d98913105fc2bec5bae6e5dd - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xb2ac2e5a3684411254d58b1c5a542212b782114d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xb0efaf46a1de55c54f333f93b1f0641e73bc16d0 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xd0fa3b5264ccde31e8b094b86bca4a1e97d3c603 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xad4c666fc170b468b19988959eb931a3676f0e9f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x790fde1fd6d2568050061a88c375d5c2e06b140b - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xaefc1edaede6adadcdf3bb344577d45a80b19582 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xa8a5356ee5d02fe33d72355e4f698782f8f199e8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x55bc964fe3b0c8cc2d4c63d65f1be7aef9bb1a3c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x95d9d28606ee55de7667f0f176ebfc3215cfd9c0 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x21b8065d10f73ee2e260e5b47d3344d3ced7596e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x0d4a11d5eeaac28ec3f61d100daf4d40471f1852 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x517f9dd285e75b599234f7221227339478d0fcc8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xa43fe16908251ee70ef74718545e4fe6c5ccec9f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x0af81cd5d9c124b4859d65697a4cd10ee223746a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xca7c2771d248dcbe09eabe0ce57a62e18da178c0 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x09d1d767edf8fa23a64c51fa559e0688e526812f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x7b73644935b8e68019ac6356c40661e1bc315860 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x180efc1349a69390ade25667487a826164c9c6e4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x9c4fe5ffd9a9fc5678cfbd93aa2d4fd684b67c4c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xa478c2975ab1ea89e8196811f51a7b7ade33eb11 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xbb2b8038a1640196fbe3e38816f3e67cba72d940 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x9ec9367b8c4dd45ec8e7b800b1f719251053ad60 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xc91ef786fbf6d62858262c82c63de45085dea659 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x197d7010147df7b99e9025c724f13723b29313f8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x25647e01bd0967c1b9599fa3521939871d1d0888 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x2f0b1417aa42ebf0b4ca1154212847f6094d708d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x6ada49aeccf6e556bb7a35ef0119cc8ca795294a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x2a6c340bcbb0a79d3deecd3bc5cbc2605ea9259f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xda2d09fbbf8ee4b5051a0e9b562c5fcb4b393b18 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x48d20b3e529fb3dd7d91293f80638df582ab2daa - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x4028daac072e492d34a3afdbef0ba7e35d8b55c4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xc2eab7d33d3cb97692ecb231a5d0e4a649cb539d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xc5be99a02c6857f9eac67bbce58df5572498f40c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xe4b8583ccb95b25737c016ac88e539d0605949e8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x8dbee21e8586ee356130074aaa789c33159921ca - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x43de4318b6eb91a7cf37975dbb574396a7b5b5c6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x9ff68f61ca5eb0c6606dc517a9d44001e564bb66 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xa29fe6ef9592b5d408cca961d0fb9b1faf497d6d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x1b1137dd16faa651e38a9dfb5d9ffff7767fdf62 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x470e8de2ebaef52014a47cb5e6af86884947f08c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x8fb8e9921922d2ffb529a95d28a0d06d275d7a59 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xd3d2e2692501a5c9ca623199d38826e513033a17 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x97e1fcb93ae7267dbafad23f7b9afaa08264cfd8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xa5e9c917b4b821e4e0a5bbefce078ab6540d6b5e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x2cc846fff0b08fb3bffad71f53a60b4b6e6d6482 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x959873fb4fc11825fba83c80c4c632db1e936e15 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xa7480aafa8ad2af3ce24ac6853f960ae6ac7f0c4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xc7e6b676bfc73ae40bcc4577f22aab1682c691c6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x570febdf89c07f256c75686caca215289bb11cfc - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x343fd171caf4f0287ae6b87d75a8964dc44516ab - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xcaa004418eb42cdf00cb057b7c9e28f0ffd840a5 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xe3d3551bb608e7665472180a20280630d9e938aa - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xb6b0c651c37ec4ca81c0a128420e02001a57fac2 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x4e34da137f0b317c633838458e0c923a5e088752 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xfe9e7931e55c514c33d489c88582fa36e84bd8e3 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x5281e311734869c64ca60ef047fd87759397efe6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x149148acc3b06b8cc73af3a10e84189243a35925 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x8ef79d6c328c25da633559c20c75f638a4863462 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x45dda9cb7c25131df268515131f647d726f50608 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x50eaedb835021e4a108b7290636d62e9765cc6d7 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x167384319b41f7094e62f7506409eb38079abff8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xa374094527e1673a86de625aa59517c5de346d32 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x86f1d8390222a3691c28938ec7404a1661e618e0 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xeda1094f59a4781456734e5d258b95e6be20b983 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x847b64f9d3a95e977d157866447a5c0a5dfa0ee5 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x94ab9e4553ffb839431e37cc79ba8905f45bfbea - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x0e44ceb592acfc5d3f09d996302eb4c499ff8c10 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x1e5bd2ab4c308396c06c182e1b7e7ba8b2935b83 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x9b08288c3be4f62bbf8d1c20ac9c5e6f9467d8b7 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xb6e57ed85c4c9dbfef2a68711e9d6f36c56e0fcb - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x3e31ab7f37c048fc6574189135d108df80f0ea26 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xd36ec33c8bed5a9f7b6630855f1533455b98a418 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xdac8a8e6dbf8c690ec6815e0ff03491b2770255d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xfe343675878100b344802a6763fd373fdeed07a4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x0a28c2f5e0e8463e047c203f00f649812ae67e4f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x88f3c15523544835ff6c738ddb30995339ad57d6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x98b9162161164de1ed182a0dfa08f5fbf0f733ca - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xeef1a9507b3d505f0062f2be9453981255b503c8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xc4c06c9a239f94fc0a1d3e04d23c159ebe8316f1 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x849ec65748107aedc518dbc42961f358ea1361a7 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x2db87c4831b2fec2e35591221455834193b50d1b - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xa4d8c89f0c20efbe54cba9e7e7a7e509056228d9 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x642f28a89fa9d0fa30e664f71804bfdd7341d21f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x2aceda63b5e958c45bd27d916ba701bc1dc08f7a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x781067ef296e5c4a4203f81c593274824b7c185d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x4ccd010148379ea531d6c587cfdd60180196f9b1 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xd866fac7db79994d08c0ca2221fee08935595b4b - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x941061770214613ba0ca3db9a700c39587bb89b6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xa9077cdb3d13f45b8b9d87c43e11bce0e73d8631 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xa01f64fa1b923dd9c5c7618b39a6ba8098a88863 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xa830ff28bb7a46570a7e43dc24a35a663b9cfc2e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x8837a61644d523cbe5216dde226f8f85e3aa9be3 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xca5d44977d6de1846530eb434167b208752fba7d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x4d05f2a005e6f36633778416764e82d1d12e7fbb - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x41e64a5bc929fa8e6a9c8d7e3b81a13b21ff3045 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x3ea34cfc9322273311f7843826a2581c4a00fd39 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x785061ed819414dc4269d2a5d5974069c0daea96 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x3f5228d0e7d75467366be7de2c31d0d098ba2c23 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x2e3f22e9a1c2470b2e293351f48c99e1fd788f32 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x2a08c38c7e1fa969325e2b64047abb085dec3756 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xe6c36eed27c2e8ecb9a233bf12da06c9730b5955 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xefa98fdf168f372e5e9e9b910fcdfd65856f3986 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x76fa081e510f43ac8335efdb4db88c9ff1894413 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xc6832ef0af793336aa44a936e54b992bff47e7cd - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x865f456479a21e2b3d866561d7171a3d0a7b112d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xbd934a7778771a7e2d9bf80596002a214d8c9304 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x9ab9f658104467604b5afa9a3e1df62f35f7b208 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x6e430d59ba145c59b73a6db674fe3d53c1f31cae - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x21b8065d10f73ee2e260e5b47d3344d3ced7596e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x0d4a11d5eeaac28ec3f61d100daf4d40471f1852 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x517f9dd285e75b599234f7221227339478d0fcc8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xa43fe16908251ee70ef74718545e4fe6c5ccec9f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x0af81cd5d9c124b4859d65697a4cd10ee223746a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xca7c2771d248dcbe09eabe0ce57a62e18da178c0 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x09d1d767edf8fa23a64c51fa559e0688e526812f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x7b73644935b8e68019ac6356c40661e1bc315860 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x180efc1349a69390ade25667487a826164c9c6e4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x9c4fe5ffd9a9fc5678cfbd93aa2d4fd684b67c4c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xa478c2975ab1ea89e8196811f51a7b7ade33eb11 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xbb2b8038a1640196fbe3e38816f3e67cba72d940 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x9ec9367b8c4dd45ec8e7b800b1f719251053ad60 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xc91ef786fbf6d62858262c82c63de45085dea659 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x197d7010147df7b99e9025c724f13723b29313f8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x25647e01bd0967c1b9599fa3521939871d1d0888 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x2f0b1417aa42ebf0b4ca1154212847f6094d708d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x6ada49aeccf6e556bb7a35ef0119cc8ca795294a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x2a6c340bcbb0a79d3deecd3bc5cbc2605ea9259f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xda2d09fbbf8ee4b5051a0e9b562c5fcb4b393b18 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x48d20b3e529fb3dd7d91293f80638df582ab2daa - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x4028daac072e492d34a3afdbef0ba7e35d8b55c4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xc2eab7d33d3cb97692ecb231a5d0e4a649cb539d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xc5be99a02c6857f9eac67bbce58df5572498f40c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xe4b8583ccb95b25737c016ac88e539d0605949e8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x8dbee21e8586ee356130074aaa789c33159921ca - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x43de4318b6eb91a7cf37975dbb574396a7b5b5c6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x9ff68f61ca5eb0c6606dc517a9d44001e564bb66 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xa29fe6ef9592b5d408cca961d0fb9b1faf497d6d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x1b1137dd16faa651e38a9dfb5d9ffff7767fdf62 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x470e8de2ebaef52014a47cb5e6af86884947f08c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x8fb8e9921922d2ffb529a95d28a0d06d275d7a59 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xd3d2e2692501a5c9ca623199d38826e513033a17 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x97e1fcb93ae7267dbafad23f7b9afaa08264cfd8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xa5e9c917b4b821e4e0a5bbefce078ab6540d6b5e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x2cc846fff0b08fb3bffad71f53a60b4b6e6d6482 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x959873fb4fc11825fba83c80c4c632db1e936e15 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xa7480aafa8ad2af3ce24ac6853f960ae6ac7f0c4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xc7e6b676bfc73ae40bcc4577f22aab1682c691c6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x570febdf89c07f256c75686caca215289bb11cfc - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x343fd171caf4f0287ae6b87d75a8964dc44516ab - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xcaa004418eb42cdf00cb057b7c9e28f0ffd840a5 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xe3d3551bb608e7665472180a20280630d9e938aa - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xb6b0c651c37ec4ca81c0a128420e02001a57fac2 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x4e34da137f0b317c633838458e0c923a5e088752 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xfe9e7931e55c514c33d489c88582fa36e84bd8e3 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x5281e311734869c64ca60ef047fd87759397efe6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x149148acc3b06b8cc73af3a10e84189243a35925 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x8ef79d6c328c25da633559c20c75f638a4863462 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x9e37cb775a047ae99fc5a24dded834127c4180cd - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x48413707b70355597404018e7c603b261fcadf3f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xade9bcd4b968ee26bed102dd43a55f6a8c2416df - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xda679706ff21114ac9fac5198bff24543f357a16 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xba3f945812a83471d709bce9c3ca699a19fb46f7 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xc9034c3e7f58003e6ae0c8438e7c8f4598d5acaa - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x4c36388be6f416a29c8d8eee81c771ce6be14b18 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xa1b2457c0b627f97f6cc892946a382451e979014 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x4b0aaf3ebb163dd45f663b38b6d93f6093ebc2d3 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xae2ce200bdb67c472030b31f602f0756c9aeb61c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x3bc5180d5439b500f381f9a46f15dd6608101671 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x5122e02898ece3bc62df8c1efdb29a9e914244d3 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x24e1cbd6fed006ceed9af0dce688acc7951d57a9 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x2556230ac694093d4d3b7b965a2f2d77d4c403a4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xdaca082c2c7d052a96fa83ea9d3a7b6839e39586 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xa555149210075702a734968f338d5e1cbd509354 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x10648ba41b8565907cfa1496765fa4d95390aa0d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x00bcec1526dae1e170a53017b8775a93b7810d7c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x20e068d76f9e90b90604500b84c7e19dcb923e7e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x6b93950a9b589bc32b82a5df4e5148f98a7fae27 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xd9caa6dbe6791fcb7fc9fb59d1a6b3dd8c1c2339 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x62e81e93136ac42a1ada48d4098f5f9e703e7455 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x84206d33845c9d811438b6fe4e7a0c634748dc50 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xd0b53d9277642d899df5c87a3966a349a798f224 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xcfa7c4bb565915f1c4f9475e2a0536d31efad776 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xa7de21f28ca460b45373b217cd4eb111c3faeff8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xb64dff20dd5c47e6dbb56ead80d23568006dec1e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xad4e969f4193878e5cc89cefb57faf6c7c0048da - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xdf5eb97e3e23ca7f5a5fd2264680377c211310ba - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xf16baaae8eb7b37f4280e72924479f69e7a61f32 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xe745a591970e0fa981204cf525e170a2b9e4fb93 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x64b74c66b9ba60ca668b781289767ae7298f37ae - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x17e1ebd791e7253a5e606fd94c5b66c14d873136 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x46715bd57b9ec01deadb35fe096fb44acda79414 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x3447accd4b8e735329d1065244aad2ed630f0122 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x2feb7f3ffc243f7de94d5ea5975533d301584e07 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x0d5959a52e7004b601f0be70618d01ac3cdce976 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x2170ca774e48a3f51559917ada6f9d7ae8f7bfea - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x62a76dfa8951aefcff787e790782db3633ebf422 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x8073679e0b3b2d1d665777cf1b2b5b1c2d3d2d0c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x143f1a6f3fb32e6ab3f22d3cc6b417b5c2197599 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x82ad659c2f152aad59bb37cbc5e7663a2de0c607 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xa4efe9e8e2a2d5a2ac46805f233b8e49d0e11955 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xfcc89a1f250d76de198767d33e1ca9138a7fb54b - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x2faa2b42b782d578a160f61bb7cd763a17476730 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xdd44c0e83c2570062d1e6fdd440b4724862e8f31 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xe3930a14641786e123e7bbe842d701fa1cbfe2df - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x6d03360ce4764e862ed81660c1f76cc2711b14b6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xc055f66f228105072315247785c00299d0ce27e8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xcae1d141ab11cef0a415cf0440025e1e5e962e06 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x21b8065d10f73ee2e260e5b47d3344d3ced7596e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x0d4a11d5eeaac28ec3f61d100daf4d40471f1852 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x517f9dd285e75b599234f7221227339478d0fcc8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xa43fe16908251ee70ef74718545e4fe6c5ccec9f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x0af81cd5d9c124b4859d65697a4cd10ee223746a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xca7c2771d248dcbe09eabe0ce57a62e18da178c0 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x09d1d767edf8fa23a64c51fa559e0688e526812f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x7b73644935b8e68019ac6356c40661e1bc315860 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x180efc1349a69390ade25667487a826164c9c6e4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x9c4fe5ffd9a9fc5678cfbd93aa2d4fd684b67c4c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xa478c2975ab1ea89e8196811f51a7b7ade33eb11 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xbb2b8038a1640196fbe3e38816f3e67cba72d940 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x9ec9367b8c4dd45ec8e7b800b1f719251053ad60 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xc91ef786fbf6d62858262c82c63de45085dea659 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x197d7010147df7b99e9025c724f13723b29313f8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x25647e01bd0967c1b9599fa3521939871d1d0888 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x2f0b1417aa42ebf0b4ca1154212847f6094d708d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x6ada49aeccf6e556bb7a35ef0119cc8ca795294a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x2a6c340bcbb0a79d3deecd3bc5cbc2605ea9259f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xda2d09fbbf8ee4b5051a0e9b562c5fcb4b393b18 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x48d20b3e529fb3dd7d91293f80638df582ab2daa - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x4028daac072e492d34a3afdbef0ba7e35d8b55c4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xc2eab7d33d3cb97692ecb231a5d0e4a649cb539d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xc5be99a02c6857f9eac67bbce58df5572498f40c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xe4b8583ccb95b25737c016ac88e539d0605949e8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x8dbee21e8586ee356130074aaa789c33159921ca - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x43de4318b6eb91a7cf37975dbb574396a7b5b5c6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x9ff68f61ca5eb0c6606dc517a9d44001e564bb66 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xa29fe6ef9592b5d408cca961d0fb9b1faf497d6d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x1b1137dd16faa651e38a9dfb5d9ffff7767fdf62 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x470e8de2ebaef52014a47cb5e6af86884947f08c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x8fb8e9921922d2ffb529a95d28a0d06d275d7a59 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xd3d2e2692501a5c9ca623199d38826e513033a17 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x97e1fcb93ae7267dbafad23f7b9afaa08264cfd8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xa5e9c917b4b821e4e0a5bbefce078ab6540d6b5e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x2cc846fff0b08fb3bffad71f53a60b4b6e6d6482 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x959873fb4fc11825fba83c80c4c632db1e936e15 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xa7480aafa8ad2af3ce24ac6853f960ae6ac7f0c4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xc7e6b676bfc73ae40bcc4577f22aab1682c691c6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x570febdf89c07f256c75686caca215289bb11cfc - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x343fd171caf4f0287ae6b87d75a8964dc44516ab - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xcaa004418eb42cdf00cb057b7c9e28f0ffd840a5 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xe3d3551bb608e7665472180a20280630d9e938aa - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xb6b0c651c37ec4ca81c0a128420e02001a57fac2 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x4e34da137f0b317c633838458e0c923a5e088752 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xfe9e7931e55c514c33d489c88582fa36e84bd8e3 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x5281e311734869c64ca60ef047fd87759397efe6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x149148acc3b06b8cc73af3a10e84189243a35925 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x8ef79d6c328c25da633559c20c75f638a4863462 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x0f338ec12d3f7c3d77a4b9fcc1f95f3fb6ad0ea6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x4eaa90264d6a3567228dcb5cfc242200da586437 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x6fe9e9de56356f7edbfcbb29fab7cd69471a4869 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xf420603317a0996a3fce1b1a80993eaef6f7ae1a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x47a90a2d92a8367a91efa1906bfc8c1e05bf10c4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x41bf5eeae051fbd2e97b76b5f8f0fdcc1a1e526b - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x28df0835942396b7a1b7ae1cd068728e6ddbbafd - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xa3f3664a52f01b42557524bd14556e379daf5669 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x1fd22fa7274bafebdfb1881321709f1219744829 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xe39cfc1a2e51a09ecbd060a24ee4eef5a97697bb - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x06396509195eb9e07c38a016694dc9ff535b128a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x5a1c486edefda2f09d3b349fadc38524f1743826 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x5bf1cf153c102a79d9e18b7fb7c79ba57fa70d0c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x2c3c320d49019d4f9a92352e947c7e5acfe47d68 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x4141325bac36affe9db165e854982230a14e6d48 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x17507bef4c3abc1bc715be723ee1baf571256e05 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x8149b92ea743cc382aada523b68b8834733b9015 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xc98f01bf2141e1140ef8f8cad99d4b021d10718f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x7f9d307973cdabe42769d9712df8ee1cc1a28d10 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x5c87da28a45e5089b762dcbbd86f743d14c54317 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x2cd97604ef77bbcb1fa0cff47545dff8ec7def08 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x7862d9b4be2156b15d54f41ee4ede2d5b0b455e4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x554548b404213c7efcdbab933f52edfe3c581834 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x63008c5ea4e47f5421e0e1428b1c5043a507d0d0 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x0350ca994791c4b07a5b02b08aaf9d6fc8ab510e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x32776ed4d96ed069a2d812773f0ad8ad9ef83cf8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x84f3ca9b7a1579ff74059bd0e8929424d3fa330e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x5289a8dbf7029ee0b0498a84777ed3941d9acfec - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xb2bc284ab4c953b7f7a06d59c0ceb2de26405f22 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x508acf810857fefa86281499068ad5d19ebce325 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xccdfcd1aac447d5b29980f64b831c532a6a33726 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x4fb87838a29b37598099ef5aa6b3fbeeef987c50 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x515e94dc736b9d8b7d28ecf1cece0aba3d75da97 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xfd6e5b7c30538dff2752058e425ad01a56b831cc - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xcb99fe720124129520f7a09ca3cbef78d58ed934 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xd2f21358c1549be193537b2a4c5dc7f0228ae011 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x93094ed1c907e4bca7eb041cb659da94f7e1b58e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xd37e6ecb991d1a0e7610c89666817665713362a7 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x73234630bd159384c8d43f145407312d64614f43 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xad1ddf00c4ae50573e4dc98e6c5ee93baa04a0c4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xa765593c821f7df9ad81119509a37961e7ffa6c5 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x9b501a7ad3087d603ceb34424b7b2a6c348ad0b7 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xafebb7cfa1a15fcac4121b609b456cbce3137c20 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x0adaf134ae0c4583b3a38fc3168a83e33162651e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xf9878a5dd55edc120fde01893ea713a4f032229c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x84e47c7f2fe86f6b5efbe14fee46b8bb871b2e05 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xf3e5bec78654049990965f666b0612e116b94fb2 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x33e59edd3214e97cb68450c6d3d6c167de072aba - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x2ca76c7e466e560e0cb11a91269bb953e41254bc - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xbb124e35ab9e85f8d59ba83500e559dc052b9368 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x21b8065d10f73ee2e260e5b47d3344d3ced7596e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x0d4a11d5eeaac28ec3f61d100daf4d40471f1852 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x517f9dd285e75b599234f7221227339478d0fcc8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xa43fe16908251ee70ef74718545e4fe6c5ccec9f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x0af81cd5d9c124b4859d65697a4cd10ee223746a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xca7c2771d248dcbe09eabe0ce57a62e18da178c0 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x09d1d767edf8fa23a64c51fa559e0688e526812f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x7b73644935b8e68019ac6356c40661e1bc315860 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x180efc1349a69390ade25667487a826164c9c6e4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x9c4fe5ffd9a9fc5678cfbd93aa2d4fd684b67c4c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xa478c2975ab1ea89e8196811f51a7b7ade33eb11 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xbb2b8038a1640196fbe3e38816f3e67cba72d940 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x9ec9367b8c4dd45ec8e7b800b1f719251053ad60 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xc91ef786fbf6d62858262c82c63de45085dea659 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x197d7010147df7b99e9025c724f13723b29313f8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x25647e01bd0967c1b9599fa3521939871d1d0888 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x2f0b1417aa42ebf0b4ca1154212847f6094d708d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x6ada49aeccf6e556bb7a35ef0119cc8ca795294a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x2a6c340bcbb0a79d3deecd3bc5cbc2605ea9259f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xda2d09fbbf8ee4b5051a0e9b562c5fcb4b393b18 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x48d20b3e529fb3dd7d91293f80638df582ab2daa - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x4028daac072e492d34a3afdbef0ba7e35d8b55c4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xc2eab7d33d3cb97692ecb231a5d0e4a649cb539d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xc5be99a02c6857f9eac67bbce58df5572498f40c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xe4b8583ccb95b25737c016ac88e539d0605949e8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x8dbee21e8586ee356130074aaa789c33159921ca - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x43de4318b6eb91a7cf37975dbb574396a7b5b5c6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x9ff68f61ca5eb0c6606dc517a9d44001e564bb66 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xa29fe6ef9592b5d408cca961d0fb9b1faf497d6d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x1b1137dd16faa651e38a9dfb5d9ffff7767fdf62 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x470e8de2ebaef52014a47cb5e6af86884947f08c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x8fb8e9921922d2ffb529a95d28a0d06d275d7a59 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xd3d2e2692501a5c9ca623199d38826e513033a17 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x97e1fcb93ae7267dbafad23f7b9afaa08264cfd8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xa5e9c917b4b821e4e0a5bbefce078ab6540d6b5e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x2cc846fff0b08fb3bffad71f53a60b4b6e6d6482 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x959873fb4fc11825fba83c80c4c632db1e936e15 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xa7480aafa8ad2af3ce24ac6853f960ae6ac7f0c4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xc7e6b676bfc73ae40bcc4577f22aab1682c691c6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x570febdf89c07f256c75686caca215289bb11cfc - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x343fd171caf4f0287ae6b87d75a8964dc44516ab - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xcaa004418eb42cdf00cb057b7c9e28f0ffd840a5 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xe3d3551bb608e7665472180a20280630d9e938aa - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xb6b0c651c37ec4ca81c0a128420e02001a57fac2 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x4e34da137f0b317c633838458e0c923a5e088752 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xfe9e7931e55c514c33d489c88582fa36e84bd8e3 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x5281e311734869c64ca60ef047fd87759397efe6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x149148acc3b06b8cc73af3a10e84189243a35925 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x8ef79d6c328c25da633559c20c75f638a4863462 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xd88d5f9e6c10e6febc9296a454f6c2589b1e8fae - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xb90fe7da36ac89448e6dfd7f2bb1e90a66659977 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xbd6313d0796984c578cae6bc5b5e23b27c5540c5 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x1f18cd7d1c7ba0dbe3d9abe0d3ec84ce1ad10066 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x7da99753ff017f1b7afb2c8c0542718dc9f15f21 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x079e7a44f42e9cd2442c3b9536244be634e8f888 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x1c8dafd358d308b880f71edb5170b010b106ca60 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xbd0f6f34baa3c1329448a69bab90111a20756f01 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x3420720e561f3082f1e514a4545f0f2e0c955a5d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xea3fb6e3313a2a90757e4ca3d6749efd0107b0b6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xf130f72f8190f662522774c3367e6e8814f5e219 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x4a46c053bd5c10a959aea258228217b9d3405f3d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xb83258bf5940c98abf54f26c5a02710bd6b83b2c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x6a209c5329f0a225fa1890d4177823c096016f34 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xdb24905b1b080f65dedb0ad978aad5c76363d3c6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xddff2cdad11898b901a661e32e9fa010780263a0 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x72dd8fe09b5b493012e5816068dfc6fb26a2a9e6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x54fc722a66abfb6500a36d8b7b2646129d0e836a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x53b612b32233c80ec439a64325a29766ce95be7f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xe5edcbe72d1bc223097a1bed1fe6c0e404b4290c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xb928c37b8bd9754d321dc3d3c6ef374d332fe761 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x2d70cbabf4d8e61d5317b62cbe912935fd94e0fe - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x953e2937f0515c43ca7995e80c84aedcbbb9385e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x84394d80830ae963b599ded7d9149b90059f182f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xa1777e082fa1746eb78dd9c1fbb515419cf6e538 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x112466c8b6e5abe42c78c47eb1b9d40baa3f943c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x9491d57c5687ab75726423b55ac2d87d1cda2c3f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x978799f1845c00c9a4d9fd2629b9ce18df66e488 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xdc55d1fd1c04e005051a40bd59c5f95623257bc5 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x34757893070b0fc5de37aaf2844255ff90f7f1e0 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x7faf167615419228f3f7d71d52d840dab154913c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xa4d7b6a50dd4c55334ca6f175dbc6561f269d264 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x0ed413cefde954d8e5c54d981d7d182b587e98e3 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x524375d0c6a04439128428f400b00eae81a2e9e4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x4b7a4530d56ff55a4dce089d917ede812e543307 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x84bb5b9bf1b6782c87cfa3e396f2f571c8e49646 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x723292eea7e1576ae482a5c317934054c0199e24 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x9b42940e8184d866aac6595a91f8d8952a59d3b9 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x37622453c614f625d288151101ffe48fd222ced1 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x4a94130b9e8eb0a0959c2c0f1ee9583213773fd9 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x51514b3dc24afc1db95586242b99f0063bea17c5 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xc130254e9196d48bbd9f91240390a6e8203132e9 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x60ac25da2ada3be14a2a8c04e45b072bed965966 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x4e392a3883a84225260ff857318517eb50e5d128 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xca0aa06385a42242fe9523cd7015f6d01cd8f6b2 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x3e448c17043ce1481bbe53c0fd19481bad8b98a6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x81060e6bf2a683f208b8799a33c7c09830cabed1 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x463fe9f646b61ccfb43a022bf947075411cd71c7 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x21b8065d10f73ee2e260e5b47d3344d3ced7596e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x0d4a11d5eeaac28ec3f61d100daf4d40471f1852 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x517f9dd285e75b599234f7221227339478d0fcc8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xa43fe16908251ee70ef74718545e4fe6c5ccec9f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x0af81cd5d9c124b4859d65697a4cd10ee223746a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xca7c2771d248dcbe09eabe0ce57a62e18da178c0 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x09d1d767edf8fa23a64c51fa559e0688e526812f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x7b73644935b8e68019ac6356c40661e1bc315860 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x180efc1349a69390ade25667487a826164c9c6e4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x9c4fe5ffd9a9fc5678cfbd93aa2d4fd684b67c4c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xa478c2975ab1ea89e8196811f51a7b7ade33eb11 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xbb2b8038a1640196fbe3e38816f3e67cba72d940 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x9ec9367b8c4dd45ec8e7b800b1f719251053ad60 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xc91ef786fbf6d62858262c82c63de45085dea659 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x197d7010147df7b99e9025c724f13723b29313f8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x25647e01bd0967c1b9599fa3521939871d1d0888 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x2f0b1417aa42ebf0b4ca1154212847f6094d708d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x6ada49aeccf6e556bb7a35ef0119cc8ca795294a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x2a6c340bcbb0a79d3deecd3bc5cbc2605ea9259f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xda2d09fbbf8ee4b5051a0e9b562c5fcb4b393b18 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x48d20b3e529fb3dd7d91293f80638df582ab2daa - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x4028daac072e492d34a3afdbef0ba7e35d8b55c4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xc2eab7d33d3cb97692ecb231a5d0e4a649cb539d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xc5be99a02c6857f9eac67bbce58df5572498f40c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xe4b8583ccb95b25737c016ac88e539d0605949e8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x8dbee21e8586ee356130074aaa789c33159921ca - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x43de4318b6eb91a7cf37975dbb574396a7b5b5c6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x9ff68f61ca5eb0c6606dc517a9d44001e564bb66 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xa29fe6ef9592b5d408cca961d0fb9b1faf497d6d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x1b1137dd16faa651e38a9dfb5d9ffff7767fdf62 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x470e8de2ebaef52014a47cb5e6af86884947f08c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x8fb8e9921922d2ffb529a95d28a0d06d275d7a59 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xd3d2e2692501a5c9ca623199d38826e513033a17 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x97e1fcb93ae7267dbafad23f7b9afaa08264cfd8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xa5e9c917b4b821e4e0a5bbefce078ab6540d6b5e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x2cc846fff0b08fb3bffad71f53a60b4b6e6d6482 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x959873fb4fc11825fba83c80c4c632db1e936e15 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xa7480aafa8ad2af3ce24ac6853f960ae6ac7f0c4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xc7e6b676bfc73ae40bcc4577f22aab1682c691c6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x570febdf89c07f256c75686caca215289bb11cfc - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x343fd171caf4f0287ae6b87d75a8964dc44516ab - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xcaa004418eb42cdf00cb057b7c9e28f0ffd840a5 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xe3d3551bb608e7665472180a20280630d9e938aa - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xb6b0c651c37ec4ca81c0a128420e02001a57fac2 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x4e34da137f0b317c633838458e0c923a5e088752 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xfe9e7931e55c514c33d489c88582fa36e84bd8e3 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x5281e311734869c64ca60ef047fd87759397efe6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x149148acc3b06b8cc73af3a10e84189243a35925 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x8ef79d6c328c25da633559c20c75f638a4863462 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x0f23d49bc92ec52ff591d091b3e16c937034496e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x0f23d49bc92ec52ff591d091b3e16c937034496e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xbf16ef186e715668aa29cef57e2fd7f9d48adfe6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x0f23d49bc92ec52ff591d091b3e16c937034496e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x5645dcb64c059aa11212707fbf4e7f984440a8cf - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x0f23d49bc92ec52ff591d091b3e16c937034496e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x3ad4913fa896391c9822a81d8d869cc0d783bdd7 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x0f23d49bc92ec52ff591d091b3e16c937034496e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x0f23d49bc92ec52ff591d091b3e16c937034496e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x0f23d49bc92ec52ff591d091b3e16c937034496e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x7a415b19932c0105c82fdb6b720bb01b0cc2cae3 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x9b3423373e6e786c9ac367120533abe4ee398373 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x4a25dbdf9629b1782c3e2c7de3bdce41f1c7f801 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xbe80225f09645f172b079394312220637c440a63 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x059615ebf32c946aaab3d44491f78e4f8e97e1d3 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x435664008f38b0650fbc1c9fc971d0a3bc2f1e47 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x4b62fa30fea125e43780dc425c2be5acb4ba743b - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xc3db44adc1fcdfd5671f555236eae49f4a8eea18 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xddd23787a6b80a794d952f5fb036d0b31a8e6aff - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xa86aca6d7c393c06dcdc30473ea3d1b05c358dff - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x1ffec7119e315b15852557f654ae0052f76e6ae1 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x0f027d40c80d8f70f77d3884776531f80b21d20e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x69c66beafb06674db41b22cfc50c34a93b8d82a2 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xeedff72a683058f8ff531e8c98575f920430fdc5 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x811cfb75567a252bea23474e2ccd1286927bfe0a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x2caccf71bdf8fff97c06a46eca29b611b1a74b5e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xf07a84f0732dfe8eea0d3961bcd8f62c761ff508 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x8c1c499b1796d7f3c2521ac37186b52de024e58c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xe5cf22ee4988d54141b77050967e1052bd9c7f7a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x7f580f8a02b759c350e6b8340e7c2d4b8162b6a9 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x48b0ab72c2591849e678e7d6f272b75ef9b863f7 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x74d0ae8b8e1fca6039707564704a25ad2ee036b0 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x5969efdde3cf5c0d9a88ae51e47d721096a97203 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xe32efff8f8b5fdc53803405aa3f623f03f8a8767 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xe8629b6a488f366d27dad801d1b5b445199e2ada - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x066b28f0c160935cf285f75ed600967bf8417035 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xddd23787a6b80a794d952f5fb036d0b31a8e6aff - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xa86aca6d7c393c06dcdc30473ea3d1b05c358dff - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x1ffec7119e315b15852557f654ae0052f76e6ae1 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x0f027d40c80d8f70f77d3884776531f80b21d20e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x69c66beafb06674db41b22cfc50c34a93b8d82a2 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xeedff72a683058f8ff531e8c98575f920430fdc5 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x811cfb75567a252bea23474e2ccd1286927bfe0a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x2caccf71bdf8fff97c06a46eca29b611b1a74b5e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xf07a84f0732dfe8eea0d3961bcd8f62c761ff508 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x8c1c499b1796d7f3c2521ac37186b52de024e58c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x146b020399769339509c98b7b353d19130c150ec - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xd28f71e383e93c570d3edfe82ebbceb35ec6c412 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xadab76dd2dca7ae080a796f0ce86170e482afb4a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x0fb07e6d6e1f52c839608e1436d2ea810cf07257 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xddd23787a6b80a794d952f5fb036d0b31a8e6aff - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xa86aca6d7c393c06dcdc30473ea3d1b05c358dff - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x1ffec7119e315b15852557f654ae0052f76e6ae1 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x0f027d40c80d8f70f77d3884776531f80b21d20e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x69c66beafb06674db41b22cfc50c34a93b8d82a2 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xeedff72a683058f8ff531e8c98575f920430fdc5 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x811cfb75567a252bea23474e2ccd1286927bfe0a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x2caccf71bdf8fff97c06a46eca29b611b1a74b5e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xf07a84f0732dfe8eea0d3961bcd8f62c761ff508 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x8c1c499b1796d7f3c2521ac37186b52de024e58c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x95d2483d2a0fff034004f91c53d649623d993896 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x19c5505638383337d2972ce68b493ad78e315147 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xc143161ed3ed8049bb63d8da42907c08a10e2269 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xc3286373599dd5af2a17a572ebb7561f05f88bec - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xbb98b3d2b18aef63a3178023a920971cf5f29be4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x647fb01a63de9a551b39c7915693b25e6bcec502 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xa90c1c009dc8292bd04ced30f9b53a5ff7a806a0 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xddd23787a6b80a794d952f5fb036d0b31a8e6aff - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xa86aca6d7c393c06dcdc30473ea3d1b05c358dff - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x1ffec7119e315b15852557f654ae0052f76e6ae1 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x0f027d40c80d8f70f77d3884776531f80b21d20e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x69c66beafb06674db41b22cfc50c34a93b8d82a2 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xeedff72a683058f8ff531e8c98575f920430fdc5 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x811cfb75567a252bea23474e2ccd1286927bfe0a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x2caccf71bdf8fff97c06a46eca29b611b1a74b5e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xf07a84f0732dfe8eea0d3961bcd8f62c761ff508 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x8c1c499b1796d7f3c2521ac37186b52de024e58c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xfb765ff72a14735550f1d798a5efd1311f2ddee7 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x3537f2a5f99f08f59eb1417073db1fadbebf0c74 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xde8ed0277ee0e84c25756a73ffa7374e4aeadf46 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xd8f3a72d2b2220a5067abe8c38aea57dc2d69a5e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x7ec18abf80e865c6799069df91073335935c4185 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x14b1911dd6b451c2771661ae8cd70637d726c356 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x9ae8084c21752971d867597c07f2673765d949a1 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xcfaf75a3d292c3535ea3acdb16ed2ee58c2bb091 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x8055e6de251e414e8393b20adab096afb3cf8399 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xffec10fe1355c2d8df4f62affcdeffdb04f06569 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xc16454420f100b2e771d8bc4c5b6200068129a34 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x046f405e4ae1d0e786eda4959adadbd417d13ad8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xeccb34691c06c1c9c31ceb2228b22cbd242b5879 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xe22a2dfaaaaec8a7b2b7acb4909eaaa5c5bd6e64 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xe2dda0911e227e73d9fd94745b851c8bc6504610 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x0f082a7870908f8cebbb2cd27a42a9225c19f898 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x69d667281778db0c3bc8177efea3a91ee95c3068 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x30d61bb28a6789f9f49d8c7fb198d63b6aba4b61 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x090f3fd9110621df127c3f9be5c6f58c02f2d5eb - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xd56f086e7b796b313d49f2bc926fac4bdd2a2b0b - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x7eb847a214192aab8fa1b503f4d4c9ddd2a08db6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x81b3bc0ef974c16d71b8614adb8c22ccc045da01 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xc9b44ca4159dbaf5722a3dc8618e9d4b5f39d5b2 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xbeef35a63fc62a3334630d9d3b4db27093d95317 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x3d5d143381916280ff91407febeb52f2b60f33cf - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x68c9325cc268df8b9ed4a06429587f28471b5f84 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xa00cc1fb7ac185222294777c6b23a13c013f07ce - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x77021e63bcbd3c5296b0cdd8a3c3770fb0ea8fa2 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xcc28456d4ff980cee3457ca809a257e52cd9cdb0 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xec0b7e8e44c9d60efd67a89dba1d4a6e02a7a4a0 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x0c8fed5dd65542ca5f0add1acab14c2e470c9110 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xd56da2b74ba826f19015e6b7dd9dae1903e85da1 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x5482c2b11951bbb92b87858242e17abde802b398 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xd95bae63641d822dc591bd4aca7a64e53eac76f9 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x06959273e9a65433de71f5a452d529544e07ddd0 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x24bf2ee2e09477082d1ddf2f0603baa460b3f5f3 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x56d8f846415e08c5e663d89505e79f522d33f947 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x548e923281f372d28a40287d3a2d30dce482fc66 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x9d744d3d905897608d24c1b8c1c7db0d30c36cd4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xddd23787a6b80a794d952f5fb036d0b31a8e6aff - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xa86aca6d7c393c06dcdc30473ea3d1b05c358dff - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x1ffec7119e315b15852557f654ae0052f76e6ae1 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x0f027d40c80d8f70f77d3884776531f80b21d20e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x69c66beafb06674db41b22cfc50c34a93b8d82a2 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xeedff72a683058f8ff531e8c98575f920430fdc5 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x811cfb75567a252bea23474e2ccd1286927bfe0a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x2caccf71bdf8fff97c06a46eca29b611b1a74b5e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0xf07a84f0732dfe8eea0d3961bcd8f62c761ff508 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/base/0x8c1c499b1796d7f3c2521ac37186b52de024e58c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xab46d39cb398fb3649ecba781180016fef75f50b - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x25048028ad87484b7fce99bc4e22dcb6c3307470 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xdb2177fee5b0ebdc7b8038cb70f3964bb6d14143 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x42d749f736051d8933b118324cded52d1f92bec1 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xb1a1b707b143b911c36e1a0f4f901c5017791aca - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x3319a81a316abd4c086f7048904e31ff86648b38 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x4a978a2d4fb7393063babfb0cee741b8bcd4dd4b - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xea403e36fb592fdfdc342c38e94284ddbb0d2105 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xe3fb01794d6912f0773171e32e723471ee8df061 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x916d7f23ccbb1d10118dcfc6ad5a10b6446ff73e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xddd23787a6b80a794d952f5fb036d0b31a8e6aff - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xa86aca6d7c393c06dcdc30473ea3d1b05c358dff - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x1ffec7119e315b15852557f654ae0052f76e6ae1 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x0f027d40c80d8f70f77d3884776531f80b21d20e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x69c66beafb06674db41b22cfc50c34a93b8d82a2 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xeedff72a683058f8ff531e8c98575f920430fdc5 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x811cfb75567a252bea23474e2ccd1286927bfe0a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x2caccf71bdf8fff97c06a46eca29b611b1a74b5e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xf07a84f0732dfe8eea0d3961bcd8f62c761ff508 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x8c1c499b1796d7f3c2521ac37186b52de024e58c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x6cde5f5a192fbf3fd84df983aa6dc30dbd9f8fac - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xd80d28850bebe6208433c298334392bc940b4fc7 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x7f7c4335ccac291ddedcef4429a626c442b627ed - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x628cb3a5a206956423d158009612813b64b19dab - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x116361f4f45e310347b43cd098fdfa459760ea7f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x5dc631ad6c26bea1a59fbf2c2680cf3df43d249f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x1a810e0b6c2dd5629afa2f0c898b9512c6f78846 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xac1cb6d3d419da9ead0b53e62d6fb4bb53473523 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x0115d04a88990889471a88e85817aac9e961c07b - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xd3409b7f3f54bb097433d0f4cd31c48ac33e569b - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x493bfc1adb2e60805693197f23132350ffd2a04e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xcf4f103759770c21f945413781ca787620316988 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xb135ebde27d366b0d62e579bae4118cb991b820e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xecbc2f008c20729b9239317408367377c5473812 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x96e0c440d3377c2dfe4f2a82add0b045e46cbe64 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x6f5304c22ac77e228e8af4732ac6677c46e09030 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xcb037f27eb3952222810966e28e0ceb650c65cd9 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xddd23787a6b80a794d952f5fb036d0b31a8e6aff - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xa86aca6d7c393c06dcdc30473ea3d1b05c358dff - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x1ffec7119e315b15852557f654ae0052f76e6ae1 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x0f027d40c80d8f70f77d3884776531f80b21d20e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x69c66beafb06674db41b22cfc50c34a93b8d82a2 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xeedff72a683058f8ff531e8c98575f920430fdc5 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x811cfb75567a252bea23474e2ccd1286927bfe0a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x2caccf71bdf8fff97c06a46eca29b611b1a74b5e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xf07a84f0732dfe8eea0d3961bcd8f62c761ff508 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x8c1c499b1796d7f3c2521ac37186b52de024e58c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0x7baece5d47f1bc5e1953fbe0e9931d54dab6d810 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/ethereum/0x6dcba3657ee750a51a13a235b4ed081317da3066 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0x83abecf7204d5afc1bea5df734f085f2535a9976 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/ethereum/0x56534741cd8b152df6d48adf7ac51f75169a83b2 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0x4eefe02fce5b53ca33c7717bbd8ad3c9cb0609f1 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/ethereum/0xe8f7c89c5efa061e340f2d2f206ec78fd8f7e124 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0xaf996125e98b5804c00ffdb4f7ff386307c99a00 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/ethereum/0x87428a53e14d24ab19c6ca4939b4df93b8996ca9 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0x7924a818013f39cf800f5589ff1f1f0def54f31f - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/ethereum/0x99950bae3d0b79b8bee86a8a208ae1b087b9dcb0 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0xb2eb5849e2606f99fc492e9add0103c667f806d3 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/ethereum/0xcbfb0745b8489973bf7b334d54fdbd573df7ef3c + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0x53c6ca2597711ca7a73b6921faf4031eedf71339 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/ethereum/0x5ab53ee1d50eef2c1dd3d5402789cd27bb52c1bb + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0x4eefe02fce5b53ca33c7717bbd8ad3c9cb0609f1 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/ethereum/0xf8e349d1d827a6edf17ee673664cfad4ca78c533 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0xaf996125e98b5804c00ffdb4f7ff386307c99a00 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/ethereum/0x202a6012894ae5c288ea824cbc8a9bfb26a49b93 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0x7924a818013f39cf800f5589ff1f1f0def54f31f - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/ethereum/0xc1cd3d0913f4633b43fcddbcd7342bc9b71c676f + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0xd35937ecd47b04a1474f8569f457fc5ac395921a - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/ethereum/0x5d27fdd96c8e4028edbabf3d667be24769425199 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0x4eefe02fce5b53ca33c7717bbd8ad3c9cb0609f1 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/ethereum/0xfb82dd4d657033133eea6e5b7015042984c5825f + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0xaf996125e98b5804c00ffdb4f7ff386307c99a00 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/ethereum/0xe333e366503f620e0242796431dc74fffd258e66 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0x7924a818013f39cf800f5589ff1f1f0def54f31f - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/ethereum/0xb2c86ff752f18499b70e8f642b3421405d50d6e9 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0x6b75f2189f0e11c52e814e09e280eb1a9a8a094a - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/ethereum/0x9dbe5dffaeb4ac2e0ac14f8b4e08b3bc55de5232 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0xb372b5abdb7c2ab8ad9e614be9835a42d0009153 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/ethereum/0xa7b3bcc6c88da2856867d29f11c67c3a85634882 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0xf369277650ad6654f25412ea8bfbd5942733babc - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/ethereum/0x543842cbfef3b3f5614b2153c28936967218a0e6 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0x4eefe02fce5b53ca33c7717bbd8ad3c9cb0609f1 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/ethereum/0x127452f3f9cdc0389b0bf59ce6131aa3bd763598 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0xaf996125e98b5804c00ffdb4f7ff386307c99a00 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/ethereum/0xdd2e0d86a45e4ef9bd490c2809e6405720cc357c + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0x7924a818013f39cf800f5589ff1f1f0def54f31f - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/ethereum/0x9a772018fbd77fcd2d25657e5c547baff3fd7d16 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0x4898cf312fbff8814cab80a8d7f6ee5ad0dc73fb - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/ethereum/0x73a38006d23517a1d383c88929b2014f8835b38b + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0x5e78afc6c804d4382bede3a0712d210e657e9b4f - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/ethereum/0xc4ce8e63921b8b6cbdb8fcb6bd64cc701fb926f2 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0x86b211ca7915a0c8d4659dd98242d9e801d88ab4 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/ethereum/0xd0a4c8a1a14530c7c9efdad0ba37e8cf4204d230 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0xb637f7c82fd774c280e23cebc725e7cd807c66d0 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/ethereum/0xc39e83fe4e412a885c0577c08eb53bdb6548004a + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0xd249c43faabc58d6dd4b0a4de598b5a956c5d8d7 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/ethereum/0xb26a868ffa4cbba926970d7ae9c6a36d088ee38c + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0x1fbae785ce68b79f7ed4f7b27c3af3ef0e0bc3d4 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/ethereum/0x0c30062368eefb96bf3ade1218e685306b8e89fa + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0x3c1376fb8487da57d4ffb263d9d01b578c7b586b - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/ethereum/0xab22d1d671bb5cee8735c5ba29ea651ccda48a8e + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0x7b24bed19856f4bb1d4c0421cfb328026cd936bd - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/ethereum/0x52c77b0cb827afbad022e6d6caf2c44452edbc39 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0x7cf887a863d81e6a483ee947dee05cb51914923c - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/ethereum/0xe46935ae80e05cdebd4a4008b6ccaa36d2845370 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0x588c8cf031809486f015908864ee8699b44017e4 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/ethereum/0xc555d55279023e732ccd32d812114caf5838fd46 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0x3987d38a4ff8520a8ef6bcc6f98d6da8bcd69b89 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/ethereum/0x3041cbd36888becc7bbcbc0045e3b1f144466f5f + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0x4eefe02fce5b53ca33c7717bbd8ad3c9cb0609f1 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/ethereum/0x7924a818013f39cf800f5589ff1f1f0def54f31f + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0xaf996125e98b5804c00ffdb4f7ff386307c99a00 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/ethereum/0xf6c4e4f339912541d3f8ed99dba64a1372af5e5b + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0x7924a818013f39cf800f5589ff1f1f0def54f31f - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/ethereum/0xb771f724c504b329623b0ce9199907137670600e + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0xde67d05242b18af00b28678db34feec883cc9cd6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/ethereum/0xaf996125e98b5804c00ffdb4f7ff386307c99a00 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x4eefe02fce5b53ca33c7717bbd8ad3c9cb0609f1 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/ethereum/0x69c7bd26512f52bf6f76fab834140d13dda673ca + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0xaf996125e98b5804c00ffdb4f7ff386307c99a00 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/ethereum/0x0b07188b12e3bba6a680e553e23c4079e98a034b + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x7924a818013f39cf800f5589ff1f1f0def54f31f - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/ethereum/0x5ced44f03ff443bbe14d8ea23bc24425fb89e3ed + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/celo/0x4a5a8b0108f446df7c1c8a459fcfb54e844b7343 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/ethereum/0xa1bf0e900fb272089c9fd299ea14bfccb1d1c2c0 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/celo/0xf6ba006abf768ab2d1b5bba2d22d9f13eb1269d4 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/ethereum/0xaf21b0ec0197e63a5c6cc30c8e947eb8165c6212 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/celo/0x4eefe02fce5b53ca33c7717bbd8ad3c9cb0609f1 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/ethereum/0x0da7096f14303eddd634c0241963c064e0244984 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/celo/0xaf996125e98b5804c00ffdb4f7ff386307c99a00 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/ethereum/0x2aeee741fa1e21120a21e57db9ee545428e683c9 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/celo/0x7924a818013f39cf800f5589ff1f1f0def54f31f - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/ethereum/0x96aa22baedc5a605357e0b9ae20ab6b10a472e03 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0x92c2fc5f306405eab0ff0958f6d85d7f8892cf4d - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/arbitrum/0x27807dd7adf218e1f4d885d54ed51c70efb9de50 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0xcbe856765eeec3fdc505ddebf9dc612da995e593 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/arbitrum/0xdd672b3b768a16b9bcb4ee1060d3e8221435beaa + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0x92c2fc5f306405eab0ff0958f6d85d7f8892cf4d - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/arbitrum/0x6f38e884725a116c9c7fbf208e79fe8828a2595f + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0xcbe856765eeec3fdc505ddebf9dc612da995e593 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/arbitrum/0xa95b0f5a65a769d82ab4f3e82842e45b8bbaf101 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0xc1738d90e2e26c35784a0d3e3d8a9f795074bca4 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/arbitrum/0x4cef551255ec96d89fec975446301b5c4e164c59 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0x92c2fc5f306405eab0ff0958f6d85d7f8892cf4d - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/arbitrum/0x421803da50d3932caa36bd1731d36a0e2af93542 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0xcbe856765eeec3fdc505ddebf9dc612da995e593 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/arbitrum/0xbbf3209130df7d19356d72eb8a193e2d9ec5c234 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0xda908c0bf14ad0b61ea5ebe671ac59b2ce091cbf - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/arbitrum/0x44af8d03393e498eec5fcfc7936ebc381f02974d + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0x254aa3a898071d6a2da0db11da73b02b4646078f - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/arbitrum/0x32a5746ba6826828716cc1a394bc33301ebc7656 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0x92c2fc5f306405eab0ff0958f6d85d7f8892cf4d - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/arbitrum/0xcb198a55e2a88841e855be4eacaad99422416b33 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0xcbe856765eeec3fdc505ddebf9dc612da995e593 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/arbitrum/0x50e7b9293aef80c304234e86c84a01be8401c530 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0x41824081f2e7beb83048bf52465ddd7c8e471da2 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/arbitrum/0xb08a8794a5d3ccca3725d92964696858d3201909 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0xa0c2ce1723b3939f47ad01a293292f2f75dc629d - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/arbitrum/0x8c9d230d45d6cfee39a6680fb7cb7e8de7ea8e71 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0xc42442f6402b68626e791a447d87b35cb1c6236e - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/arbitrum/0x53c6ca2597711ca7a73b6921faf4031eedf71339 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0x84537db6f6aaa2afdb71f325d14b9f5f7825bef1 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/arbitrum/0x4f122edcd91af8cda38c3a87158afa8687bab57c + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0x13933689ed2c6c66e83aed64336df14896efb7e2 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/arbitrum/0x4fd47e5102dfbf95541f64ed6fe13d4ed26d2546 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0x92c2fc5f306405eab0ff0958f6d85d7f8892cf4d - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/arbitrum/0xa961f0473da4864c5ed28e00fcc53a3aab056c1b + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0xcbe856765eeec3fdc505ddebf9dc612da995e593 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/arbitrum/0x663b1d43c27e41e5e512bf59010133997d1cd304 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x039df62583ddc1c5fda75db152b87113d863b6d6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/arbitrum/0xc0f05732d1cda6f59487ceeef4390abcad86ea3e + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x92c2fc5f306405eab0ff0958f6d85d7f8892cf4d - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/arbitrum/0x52f9d14bed8ce6536da063aaf274ae2747ef4853 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0xcbe856765eeec3fdc505ddebf9dc612da995e593 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/arbitrum/0x52c77b0cb827afbad022e6d6caf2c44452edbc39 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/celo/0x92c2fc5f306405eab0ff0958f6d85d7f8892cf4d - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/arbitrum/0xe46935ae80e05cdebd4a4008b6ccaa36d2845370 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/celo/0xcbe856765eeec3fdc505ddebf9dc612da995e593 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/arbitrum/0xc555d55279023e732ccd32d812114caf5838fd46 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0xc39e83fe4e412a885c0577c08eb53bdb6548004a - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/arbitrum/0x3041cbd36888becc7bbcbc0045e3b1f144466f5f + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0xdbac78be00503d10ae0074e5e5873a61fc56647c - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/arbitrum/0x7924a818013f39cf800f5589ff1f1f0def54f31f + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0xc1cd3d0913f4633b43fcddbcd7342bc9b71c676f - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/arbitrum/0xf6c4e4f339912541d3f8ed99dba64a1372af5e5b + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0x6c4c7f46d9d4ef6bc5c9e155f011ad19fc4ef321 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/arbitrum/0xb771f724c504b329623b0ce9199907137670600e + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0xb2c86ff752f18499b70e8f642b3421405d50d6e9 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/arbitrum/0xaf996125e98b5804c00ffdb4f7ff386307c99a00 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0x16588709ca8f7b84829b43cc1c5cb7e84a321b16 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/arbitrum/0x69c7bd26512f52bf6f76fab834140d13dda673ca + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0xd0a4c8a1a14530c7c9efdad0ba37e8cf4204d230 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/arbitrum/0x0b07188b12e3bba6a680e553e23c4079e98a034b + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0xf92f2e3fca01491baba0975264362cc38b1cab7b - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/arbitrum/0x5ced44f03ff443bbe14d8ea23bc24425fb89e3ed + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0x3e6e23198679419cd73bb6376518dcc5168c8260 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/arbitrum/0xa1bf0e900fb272089c9fd299ea14bfccb1d1c2c0 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0x531b6a4b3f962208ea8ed5268c642c84bb29be0b - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/arbitrum/0xaf21b0ec0197e63a5c6cc30c8e947eb8165c6212 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0x553e9c493678d8606d6a5ba284643db2110df823 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/arbitrum/0x0da7096f14303eddd634c0241963c064e0244984 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0xe3170d65018882a336743a9c396c52ea4b9c5563 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/arbitrum/0x2aeee741fa1e21120a21e57db9ee545428e683c9 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0x1385fc1fe0418ea0b4fcf7adc61fc7535ab7f80d - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/arbitrum/0x96aa22baedc5a605357e0b9ae20ab6b10a472e03 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0x5cd0ad98ba6288ed7819246a1ebc0386c32c314b - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/optimism/0xe612cb2b5644aef0ad3e922bae70a8374c63515f + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0xe945683b3462d2603a18bdfbb19261c6a4f03ad1 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/optimism/0xa39fe8f7a00ce28b572617d3a0bc1c2b44110e79 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0xa1bf0e900fb272089c9fd299ea14bfccb1d1c2c0 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/optimism/0xc4329493d7566525a4d51698a33f43ad240e9290 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0xe46935ae80e05cdebd4a4008b6ccaa36d2845370 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/optimism/0xd4cb5566b5c16ef2f4a08b1438052013171212a2 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0x3041cbd36888becc7bbcbc0045e3b1f144466f5f - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/optimism/0x9c92ed19a86986124447a73b27625230dd52f805 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0x0ad1e922e764df5ab6d636f5d21ecc2e41e827f0 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/optimism/0x5e2cd0da3411449152010d8b7f2b624eb29cca59 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0xe945683b3462d2603a18bdfbb19261c6a4f03ad1 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/optimism/0xc1738d90e2e26c35784a0d3e3d8a9f795074bca4 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0xa1bf0e900fb272089c9fd299ea14bfccb1d1c2c0 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/optimism/0x6b3a3d6ed64faf933a7a4b1bd44b2efba47614ac + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0xe46935ae80e05cdebd4a4008b6ccaa36d2845370 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/optimism/0x85e8d0fddf559a57aac6404e7695142cd53eb808 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0x3041cbd36888becc7bbcbc0045e3b1f144466f5f - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/optimism/0x87dddd2e152bf1955e7e03d9f23a9dcc163eebf6 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0x6b3a3d6ed64faf933a7a4b1bd44b2efba47614ac - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/optimism/0x679420c54cc4806d0f480925772965746d9f9779 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0x4ce4a1a593ea9f2e6b2c05016a00a2d300c9ffd8 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/optimism/0x740601243a6aa25ce4ee2d196eef83ac3bec6c65 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0x0843e0f56b9e7fdc4fb95fabba22a01ef4088f41 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/optimism/0x52c77b0cb827afbad022e6d6caf2c44452edbc39 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0x8323d063b1d12acce4742f1e3ed9bc46d71f4222 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/optimism/0xe46935ae80e05cdebd4a4008b6ccaa36d2845370 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0xe945683b3462d2603a18bdfbb19261c6a4f03ad1 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/optimism/0xc555d55279023e732ccd32d812114caf5838fd46 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0xa1bf0e900fb272089c9fd299ea14bfccb1d1c2c0 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/optimism/0x3041cbd36888becc7bbcbc0045e3b1f144466f5f + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0xe46935ae80e05cdebd4a4008b6ccaa36d2845370 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/optimism/0x7924a818013f39cf800f5589ff1f1f0def54f31f + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0x3041cbd36888becc7bbcbc0045e3b1f144466f5f - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/optimism/0xf6c4e4f339912541d3f8ed99dba64a1372af5e5b + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0xe30e4dfdbb10949c27501922f845e20cfa579f09 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/optimism/0xb771f724c504b329623b0ce9199907137670600e + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0x7e02ae3f794ebade542c92973eb1c46d7e2e935d - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/optimism/0xaf996125e98b5804c00ffdb4f7ff386307c99a00 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0xfa22d298e3b0bc1752e5ef2849cec1149d596674 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/optimism/0x69c7bd26512f52bf6f76fab834140d13dda673ca + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0x8066ee17156e4184d69277e26fa8cbca3a845edf - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/optimism/0x0b07188b12e3bba6a680e553e23c4079e98a034b + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0x418de8e0ab58abfe916a47821a055c59b9502deb - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/optimism/0x5ced44f03ff443bbe14d8ea23bc24425fb89e3ed + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0xfb9caae5a5c0ab91f68542124c05d1efbb97d151 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/optimism/0xa1bf0e900fb272089c9fd299ea14bfccb1d1c2c0 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0xb68606a75b117906e06caa0755896ad2b3dd0272 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/optimism/0xaf21b0ec0197e63a5c6cc30c8e947eb8165c6212 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0x6e33c0f5e16b45114679eac217e0c0138cefcd2e - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/optimism/0x0da7096f14303eddd634c0241963c064e0244984 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0xd64fb39a5681908ad488b487d65f5d8479cb235c - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/optimism/0x2aeee741fa1e21120a21e57db9ee545428e683c9 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0xe945683b3462d2603a18bdfbb19261c6a4f03ad1 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/optimism/0x96aa22baedc5a605357e0b9ae20ab6b10a472e03 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0xa1bf0e900fb272089c9fd299ea14bfccb1d1c2c0 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/polygon/0xe30e4dfdbb10949c27501922f845e20cfa579f09 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0xe46935ae80e05cdebd4a4008b6ccaa36d2845370 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/polygon/0xda67d7c01c4c8f757c105c0890d94ac489952cd5 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0x3041cbd36888becc7bbcbc0045e3b1f144466f5f - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/polygon/0xda908c0bf14ad0b61ea5ebe671ac59b2ce091cbf + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0x0217fc17c642d29b890bcf888e21be2378493e01 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/polygon/0x254aa3a898071d6a2da0db11da73b02b4646078f + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0x099d23a43da5a8a9282266dbefeaaef958150300 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/polygon/0xd9abecb39a5885d1e531ed3599adfed620e2fc8a + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0xd92e0767473d1e3ff11ac036f2b1db90ad0ae55f - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/polygon/0x1f6082db7c8f4b199e17090cd5c8831a1dad1997 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0xe945683b3462d2603a18bdfbb19261c6a4f03ad1 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/polygon/0x25fb97799f80433e422f47e75173314e54dae174 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0xa1bf0e900fb272089c9fd299ea14bfccb1d1c2c0 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/polygon/0xf369277650ad6654f25412ea8bfbd5942733babc + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0xe46935ae80e05cdebd4a4008b6ccaa36d2845370 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/polygon/0x22d1c3c541dd649ea4a8709fc787f348dc069e95 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0x3041cbd36888becc7bbcbc0045e3b1f144466f5f - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/polygon/0x357faf5843c7fd7fb4e34fbeabdac16eabe8a5bc + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x40c547e7fd88f60d94788953b83d9342d8d133c6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/polygon/0x019c29d5c97f8cbaa67013e2cf4b6506a5cf183a + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x397433498c7befde4b4049b98a7ff081a2c17387 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/polygon/0x8066ee17156e4184d69277e26fa8cbca3a845edf + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0xf9be03505869d719ba194757943575ed2af001f2 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/polygon/0x88aaeed1fcfca2eda30749afa9ad45a75c80e292 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x18c40bb9281a07627ff25cea45b7511f68fd0076 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/polygon/0x0a63d3910ffc1529190e80e10855c4216407cc45 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x270d89e983d9821a418bf193684736414fab78c5 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/polygon/0x50defb73a76efe5d5d35cf267ffb02dfd6cd96bc + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0xb125aa15ad943d96e813e4a06d0c34716f897e26 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/polygon/0x14653ce9f406ba7f35a7ffa43c81fa7ecd99c788 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x813c0decbb1097fff46d0ed6a39fb5f6a83043f4 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/polygon/0x258a4b7373f6863db5a17de191e0cebb1e0bbc8a + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x9a7ac628ba9f330341486380af729c8975388959 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/polygon/0x96239bd7ae3d9bc253b1cc7cf7a84f3a67ca5369 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0xf2c9339945bff71dd0bffd3c142164112cd05dc6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/polygon/0x56fcb902bee19a645f9607cd1e1c0737b6358feb + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x12a4619c0bd9710732fbc458e9baa73df6c3d35f - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/polygon/0x52c77b0cb827afbad022e6d6caf2c44452edbc39 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x96530dac7817f186390b64ba63d13becd079b28d - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/polygon/0xe46935ae80e05cdebd4a4008b6ccaa36d2845370 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x18fc1e95adb68b556212ebbad777f3fbb644db98 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/polygon/0xc555d55279023e732ccd32d812114caf5838fd46 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0xabbeb324b090550ca6d15ec71019915813f54f90 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/polygon/0x3041cbd36888becc7bbcbc0045e3b1f144466f5f + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x86d708404d0db1d97843e66d4ed6b86d11be705b - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/polygon/0x7924a818013f39cf800f5589ff1f1f0def54f31f + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0xbfbba3de6a260c8374f8299c38898312c2d6e9a6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/polygon/0xf6c4e4f339912541d3f8ed99dba64a1372af5e5b + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0xe945683b3462d2603a18bdfbb19261c6a4f03ad1 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/polygon/0xb771f724c504b329623b0ce9199907137670600e + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0xa1bf0e900fb272089c9fd299ea14bfccb1d1c2c0 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/polygon/0xaf996125e98b5804c00ffdb4f7ff386307c99a00 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0xe46935ae80e05cdebd4a4008b6ccaa36d2845370 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/polygon/0x69c7bd26512f52bf6f76fab834140d13dda673ca + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x3041cbd36888becc7bbcbc0045e3b1f144466f5f - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/polygon/0x0b07188b12e3bba6a680e553e23c4079e98a034b + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/celo/0xe945683b3462d2603a18bdfbb19261c6a4f03ad1 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/polygon/0x5ced44f03ff443bbe14d8ea23bc24425fb89e3ed + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/celo/0xa1bf0e900fb272089c9fd299ea14bfccb1d1c2c0 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/polygon/0xa1bf0e900fb272089c9fd299ea14bfccb1d1c2c0 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/celo/0xe46935ae80e05cdebd4a4008b6ccaa36d2845370 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/polygon/0xaf21b0ec0197e63a5c6cc30c8e947eb8165c6212 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/celo/0x3041cbd36888becc7bbcbc0045e3b1f144466f5f - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/polygon/0x0da7096f14303eddd634c0241963c064e0244984 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0xd31d41dffa3589bb0c0183e46a1eed983a5e5978 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/polygon/0x2aeee741fa1e21120a21e57db9ee545428e683c9 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0x391e8501b626c623d39474afca6f9e46c2686649 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/polygon/0x96aa22baedc5a605357e0b9ae20ab6b10a472e03 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0xd0fc8ba7e267f2bc56044a7715a489d851dc6d78 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0xd364eb55e17700b54bd75feb3f14582ed7a29444 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0x4fd47e5102dfbf95541f64ed6fe13d4ed26d2546 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0xfbb6eed8e7aa03b138556eedaf5d271a5e1e43ef + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0xe9033c0011f35547fa90d3f8a6ad4b666a590759 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0x6c561b446416e1a00e8e93e221854d6ea4171372 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0x0c3561d3b72e17378d99684414aa8669daeb8bd0 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0x8c7080564b5a792a33ef2fd473fba6364d5495e5 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0x14653ce9f406ba7f35a7ffa43c81fa7ecd99c788 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0xd2fdfd5059a83e15bf362f094a2ae63f03b554ca + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0x3204e9734a56a4d7c6f4f5822e14182d9d1a43c4 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0x68b27e9066d3aadc6078e17c8611b37868f96a1d + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0x43faefd4c0c25e969ac211cd97a4a51e52c729b7 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0x037818b04ac34ea8b54b6683b79ef24d23c0e7cb + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0xa652ab3be697c7a01fbdce4d73f8e8acd990251c - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0xbf6ef625de5df898cc1d0f91868aae03976a2e2d + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0x29962083891241aad61ad97bae46d032c9c0c55c - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0x183ea22691c54806fe96555436dd312b6befac2f + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0x26bf3601b77be9c31b13b22ebca02914db9c7468 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0x12146c8e7469be19ec6c7f58b80246548144f8b8 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0x0d2edd335982f56662d772b93d86901eb9bd2ff9 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0x7aea2e8a3843516afa07293a10ac8e49906dabd1 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0xbaed273edd493930711fe88690ebd1f30f7f55ab - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0xc1a6fbedae68e1472dbb91fe29b51f7a0bd44f97 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x16033643947bf4d8a1ae37b055edf57cb183106a - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0xaec085e5a5ce8d96a7bdd3eb3a62445d4f6ce703 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/celo/0xf59abf32c1e8c5d2c6e3faa2131533bbcd466194 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0xa23fab21d0653c231166b31cb6274ff45eba2ee5 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/celo/0x0312187403bf72b8d2d80729894d6ac3300bd63f - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0x4fe87203b27a105a772f195d3f30dea714d1ecf0 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0x416fdbc4fb8d4d1f48d0d3778c59dfa5352e9b15 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0x61928bf5f2895b682ecc9b13957aa5a5fe040cc0 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0x90908e414d3525e33733d320798b5681508255ea - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0xb3fb7ccf7b681e9562c6da467db4859a8ef0b8de + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0x5918aca9ae924e6eaaa3d293bb92bdec9ab79338 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0xfdbaf04326acc24e3d1788333826b71e3291863a + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0x8270e64d22cf13e92c641c4006408c7d7e3ff341 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0x0fb597d6cfe5be0d5258a7f017599c2a4ece34c7 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0x16503510c58da73486950b72a12ead3d1d8355dd - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0x7b9fda92bfa6fdadfdc4f6c72c0cc8336e7d7497 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0x90908e414d3525e33733d320798b5681508255ea - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0xf1fdc83c3a336bdbdc9fb06e318b08eaddc82ff4 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0x7505159f644ddc5eae21c119e328d0d5bee574b0 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0x1d69099803b4580efb8df0c7ef083e550a1c42c1 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0xe870bfe4aacb6e234b645e535d26c53790d50e78 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0xa213c82265cd3d94f972f735a4f5130e34df81bc + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0x2e2d190ad4e0d7be9569baebd4d33298379b0502 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0x8c7adf7bcfdfca0a27f3d7ad49698b9e11c1f20b + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0x90908e414d3525e33733d320798b5681508255ea - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0x5116773e18a9c7bb03ebb961b38678e45e238923 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0xb834093d7e46f7644be45e77281394d31003e866 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0xb3adde966b8a1a6f22a04914ee9fe0798e71fc5b + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0xb5a1fd804342cfb679bd8ada75718bc3ec43097e - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0x9399da51c1a85e64cce4b30b554875d2b89b2445 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0x90908e414d3525e33733d320798b5681508255ea - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0x43f34a518e20b9454c94bf4026ec9024ed84a062 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0x9e71e2b14d7e6d30811628ab0965f28e4e2edbce - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0x9c087eb773291e50cf6c6a90ef0f4500e349b903 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0xa011da4a0c9261ecf4694bf73a74d113aa261133 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0x670e77c361375be9013869ccc516027ccc90383f + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0x7ab922c1bfdf7df977c7531c5782074d866f3adc - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0x0eb7fbe43045426938ddadc11dc41338e0907659 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0xe2d2050430e341a8f3988e2726e44d9370f8cd3a - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0x76bf0abd20f1e0155ce40a62615a90a709a6c3d8 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0xed66ba3ea44425805a085b1ca80d00467b055b38 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0x52c77b0cb827afbad022e6d6caf2c44452edbc39 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0x40dade19adc198125ec237a2c48b3408568b2f81 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0xe46935ae80e05cdebd4a4008b6ccaa36d2845370 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0x166bc40da621d3cb978e24334f844b84ddef25f8 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0xc555d55279023e732ccd32d812114caf5838fd46 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0x76bf0abd20f1e0155ce40a62615a90a709a6c3d8 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0x3041cbd36888becc7bbcbc0045e3b1f144466f5f + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0x90908e414d3525e33733d320798b5681508255ea - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0x7924a818013f39cf800f5589ff1f1f0def54f31f + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x6948d6c8532c6b0006cb67c6fb9c399792c8ac91 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0xf6c4e4f339912541d3f8ed99dba64a1372af5e5b + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x90908e414d3525e33733d320798b5681508255ea - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0xb771f724c504b329623b0ce9199907137670600e + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/celo/0x4e40cf4a7d8724e5adc2b791bbf9451d1e260b93 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0xaf996125e98b5804c00ffdb4f7ff386307c99a00 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/celo/0x90908e414d3525e33733d320798b5681508255ea - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0x69c7bd26512f52bf6f76fab834140d13dda673ca + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0xc0067d751fb1172dbab1fa003efe214ee8f419b6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0x0b07188b12e3bba6a680e553e23c4079e98a034b + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0xc3d7aa944105d3fafe07fc1822102449c916a8d0 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0x5ced44f03ff443bbe14d8ea23bc24425fb89e3ed + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0xd6b4cce96ddf8aab2e5750983af9a901f17fbc36 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0xa1bf0e900fb272089c9fd299ea14bfccb1d1c2c0 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0x4cef551255ec96d89fec975446301b5c4e164c59 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0xaf21b0ec0197e63a5c6cc30c8e947eb8165c6212 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0xc0067d751fb1172dbab1fa003efe214ee8f419b6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0x0da7096f14303eddd634c0241963c064e0244984 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0xc3d7aa944105d3fafe07fc1822102449c916a8d0 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0x2aeee741fa1e21120a21e57db9ee545428e683c9 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0xdd0c6bae8ad5998c358b823df15a2a4181da1b80 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/base/0x96aa22baedc5a605357e0b9ae20ab6b10a472e03 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0xc0067d751fb1172dbab1fa003efe214ee8f419b6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/bnb/0xfe4fe5b4575c036ac6d5cccfe13660020270e27a + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0xc3d7aa944105d3fafe07fc1822102449c916a8d0 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/bnb/0x813c0decbb1097fff46d0ed6a39fb5f6a83043f4 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0xc0067d751fb1172dbab1fa003efe214ee8f419b6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/bnb/0xa3adcaaf941a01eec47655c550dd5504637d029a + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0xc3d7aa944105d3fafe07fc1822102449c916a8d0 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/bnb/0xb0bb2c1d32c7b27f21eec4402c6d1c38795c090a + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0x5e6ff2fa4ca244b6b33c7286d368120822eacc11 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/bnb/0xfa7d79f971a70771e5e92bd80ab955edc8602f4d + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0x98efd62b4bfbde6393b18b063c506ce5a77f4810 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/bnb/0x26b4103c8da21725909955fe85f7f6249d05dd9e + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0x3c5096df639262db0a6cd0172f08709d4161094b - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/bnb/0xafa5421fe7997c16e11458659f5a87d67f1e8651 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0xae31f0e673fc5f33cfc0e9abb426d8051404a7c5 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/bnb/0x86d708404d0db1d97843e66d4ed6b86d11be705b + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0xc0067d751fb1172dbab1fa003efe214ee8f419b6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/bnb/0x4d170f8714367c44787ae98259ce8adb72240067 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0xc3d7aa944105d3fafe07fc1822102449c916a8d0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0xc0067d751fb1172dbab1fa003efe214ee8f419b6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0xc3d7aa944105d3fafe07fc1822102449c916a8d0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0xd10456ce05b9af05c8eede0f93ea8aa80a0daa2f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0x065c22a16f6531706681fabbc8df135fe6eb1c2e - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/bnb/0x7e4fb9e08cf122feb925117bace017ea234944d3 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/celo/0x8ab8d851c6b31d8a4d42fd7d3e47b20861b025f2 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/bnb/0x4094915f7849b26e8d43dee1f7e3b7b477a0b5bb + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/celo/0xc0067d751fb1172dbab1fa003efe214ee8f419b6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/bnb/0x039df62583ddc1c5fda75db152b87113d863b6d6 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/celo/0xc3d7aa944105d3fafe07fc1822102449c916a8d0 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/bnb/0xb125aa15ad943d96e813e4a06d0c34716f897e26 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0x2982d3295a0e1a99e6e88ece0e93ffdfc5c761ae - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/bnb/0xb007dda6ca7a57785ce04981c30a1934995a197a + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0xc593fe9193b745447e86b45ea0bf62565ee030cc - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/bnb/0x16033643947bf4d8a1ae37b055edf57cb183106a + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0x88051b0eea095007d3bef21ab287be961f3d8598 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/bnb/0x4cd04970dcad72d09c3af2e09f15bbcd2eb1d5d4 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0xaf21b0ec0197e63a5c6cc30c8e947eb8165c6212 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/bnb/0xf54eba95d7f8dbe4bfeb0b6e038b3c2bedd3e40a + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0x9c84f58bb51fabd18698efe95f5bab4f33e96e8f - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/bnb/0x9a7ac628ba9f330341486380af729c8975388959 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0xb31273fd2dfc05e6fd91a3b8a2a681aeb0fbcf48 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/bnb/0x7ef0a523c49b1dd07e3593198c5260a95ad7859a + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0xaf7b48ae2f4773fd44f9208cca3db5ae7bfa7e37 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/bnb/0x70c132a2ddeccf0d76cc9b64a749ffe375a79a21 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0xc2125a452115ff5a300cc2a6ffae99637f6e329d - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/bnb/0x026428e531f30b2714ceff3781d7cdf5d278e96a + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0xb08a8794a5d3ccca3725d92964696858d3201909 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/bnb/0x96d5d78b179169ee0a0a0104dc514988f2a797fe + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0xaf21b0ec0197e63a5c6cc30c8e947eb8165c6212 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/bnb/0x3373a22cb07cb49651b82cf6f174ef434e4dbaa8 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0x9c84f58bb51fabd18698efe95f5bab4f33e96e8f - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/bnb/0x52c77b0cb827afbad022e6d6caf2c44452edbc39 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0xae99efe6b04bbe5b8b4ad567946fb84b35681abb - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/bnb/0xe46935ae80e05cdebd4a4008b6ccaa36d2845370 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0xaf21b0ec0197e63a5c6cc30c8e947eb8165c6212 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/bnb/0xc555d55279023e732ccd32d812114caf5838fd46 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0x9c84f58bb51fabd18698efe95f5bab4f33e96e8f - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/bnb/0x3041cbd36888becc7bbcbc0045e3b1f144466f5f + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0xaf21b0ec0197e63a5c6cc30c8e947eb8165c6212 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/bnb/0x7924a818013f39cf800f5589ff1f1f0def54f31f + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0x9c84f58bb51fabd18698efe95f5bab4f33e96e8f - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/bnb/0xf6c4e4f339912541d3f8ed99dba64a1372af5e5b + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0x6696710b8e3dc0d844c8b9244767962a4a61ad97 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/bnb/0xb771f724c504b329623b0ce9199907137670600e + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0xcde77ef185a8f886d03b109573cc1dcdcf3cf1f8 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/bnb/0xaf996125e98b5804c00ffdb4f7ff386307c99a00 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0xaf21b0ec0197e63a5c6cc30c8e947eb8165c6212 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/bnb/0x69c7bd26512f52bf6f76fab834140d13dda673ca + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0x9c84f58bb51fabd18698efe95f5bab4f33e96e8f - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/bnb/0x0b07188b12e3bba6a680e553e23c4079e98a034b + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x35f5387decce5a234da1a32ca3c9e338a48bcf37 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/bnb/0x5ced44f03ff443bbe14d8ea23bc24425fb89e3ed + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x4178dd7eb2eb983ba7f7e41648cf91db6be20190 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/bnb/0xa1bf0e900fb272089c9fd299ea14bfccb1d1c2c0 + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xaf21b0ec0197e63a5c6cc30c8e947eb8165c6212 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x9c84f58bb51fabd18698efe95f5bab4f33e96e8f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0xb6c8f9490314394cfc6edacb8717bfdc1eb8dab5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0x1625fe58cdb3726e5841fb2bb367dde9aaa009b3 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0xb1ed164c736909ba7ddbc1feb7ced4eaad854a87 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0x95faa9a91cd6c1c018e4b1a6fc4c89d4f1695e5d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0xa143ccf73c25eec6f38bd1b741043ebea228b8e9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0x2e067e0eab7fd31c01473c0f56f3295afb82e461 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0xbc83c60e853398d263c1d88899cf5a8b408f9654 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0xaf21b0ec0197e63a5c6cc30c8e947eb8165c6212 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0x9c84f58bb51fabd18698efe95f5bab4f33e96e8f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x202a6012894ae5c288ea824cbc8a9bfb26a49b93 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x744159757cac173a7a3ecf5e97adb10d1a725377 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x127452f3f9cdc0389b0bf59ce6131aa3bd763598 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x5ced44f03ff443bbe14d8ea23bc24425fb89e3ed - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x5ced44f03ff443bbe14d8ea23bc24425fb89e3ed - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0x2264ba9dc0b257c69eeae7782e8ff608cc65d6a7 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0x5ced44f03ff443bbe14d8ea23bc24425fb89e3ed - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0x00a59c2d0f0f4837028d47a391decbffc1e10608 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0x5ced44f03ff443bbe14d8ea23bc24425fb89e3ed - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0xad6e8f6a34087bddfb03815e2c10e4f7bfd4395b - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/bnb/0x0da7096f14303eddd634c0241963c064e0244984 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0xd5bb156cb73bfca62f68dc3dff7e5ec4e305b861 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/bnb/0x2aeee741fa1e21120a21e57db9ee545428e683c9 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0xc0d8f259578c985947a050802fb4857261af0bf3 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/bnb/0x96aa22baedc5a605357e0b9ae20ab6b10a472e03 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0x5ced44f03ff443bbe14d8ea23bc24425fb89e3ed - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/celo/0x7b9a5bc920610f54881f2f6359007957de504862 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x74f7a360eb36a46b675ea932ea07094a3ace441f - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/celo/0xb466d5429d6ad9999bf112c225d9d7b15e96c658 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x626761cc5b9fafe4696bf8def4aa015576bb4bef - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/celo/0xf27d0dac09460b236d4d9e0da316fe9c3a99b4a2 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x5ced44f03ff443bbe14d8ea23bc24425fb89e3ed - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/celo/0x95faa9a91cd6c1c018e4b1a6fc4c89d4f1695e5d + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/celo/0xc767c0b2e2e56c455fd29f9ee9b6e6f035c71ed4 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/celo/0xb6c8f9490314394cfc6edacb8717bfdc1eb8dab5 + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0x625cb959213d18a9853973c2220df7287f1e5b7d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0x5ced44f03ff443bbe14d8ea23bc24425fb89e3ed - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x7138eae57e8a214f7297e5e67bb6e183df3572d5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0xc7bbec68d12a0d1830360f8ec58fa599ba1b0e9b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x840deeef2f115cf50da625f7368c24af6fe74410 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x69c7bd26512f52bf6f76fab834140d13dda673ca - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x69c7bd26512f52bf6f76fab834140d13dda673ca - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0x69c7bd26512f52bf6f76fab834140d13dda673ca - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0xda71299ff6bdac31bdcafde52a41d460f17e3ad9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0xabebc245a9a47166ecd10933d43817c8ef6fb825 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0x69c7bd26512f52bf6f76fab834140d13dda673ca - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x69c7bd26512f52bf6f76fab834140d13dda673ca - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0xb007dda6ca7a57785ce04981c30a1934995a197a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x0de383928e4fcf0f90ad2d6a5ee18eb3b9d16a55 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/celo/0x1625fe58cdb3726e5841fb2bb367dde9aaa009b3 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x0a36df020fe3f132e6557899f272bf3d4591620e - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/celo/0xa143ccf73c25eec6f38bd1b741043ebea228b8e9 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x69c7bd26512f52bf6f76fab834140d13dda673ca - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/celo/0xc767c0b2e2e56c455fd29f9ee9b6e6f035c71ed4 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/celo/0x69c7bd26512f52bf6f76fab834140d13dda673ca - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/celo/0xaa97f0689660ea15b7d6f84f2e5250b63f2b381a + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0x8c9d230d45d6cfee39a6680fb7cb7e8de7ea8e71 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/celo/0xb1ed164c736909ba7ddbc1feb7ced4eaad854a87 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0xcb198a55e2a88841e855be4eacaad99422416b33 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/celo/0x05efb437e4e97efea6450321eca8d7585a731369 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0x9b371948735f612be19195f5f6e5ebc03839cdaf - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/celo/0x065c22a16f6531706681fabbc8df135fe6eb1c2e + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0xb3709d0e16b618b15ee4bcf82d19b9e7d4100914 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/celo/0xbc83c60e853398d263c1d88899cf5a8b408f9654 + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xe426e1305f5e6093864762bf9d2d8b44bc211c59 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0x7b9a5bc920610f54881f2f6359007957de504862 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0xfb82dd4d657033133eea6e5b7015042984c5825f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x92560c178ce069cc014138ed3c2f5221ba71f58a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x6ef7d514d75b5a5a3c500dba1b161a81e842e7a4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0xac70bd92f89e6739b3a08db9b6081a923912f73d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x1ebcf8831b93450ea81b0619c5e05b98751c8322 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x470d0d72c975a7f328bd63808bfffd28194b3eb6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0xa961f0473da4864c5ed28e00fcc53a3aab056c1b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x6ef7d514d75b5a5a3c500dba1b161a81e842e7a4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0xa5b6d588ceb3aa1bf543d095038479188f884690 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0x6ef7d514d75b5a5a3c500dba1b161a81e842e7a4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0x6ef7d514d75b5a5a3c500dba1b161a81e842e7a4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0xb1419a7f9e8c6e434b1d05377e0dbc4154e3de78 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0x6ef7d514d75b5a5a3c500dba1b161a81e842e7a4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x70c132a2ddeccf0d76cc9b64a749ffe375a79a21 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x6ef7d514d75b5a5a3c500dba1b161a81e842e7a4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0x6ef7d514d75b5a5a3c500dba1b161a81e842e7a4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x1b942ce8bf08290f740b9e825c91e07fcd0bfe75 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x5016cd7b785a773f7f3a3ff4035a1e7a76543946 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/celo/0x8ab8d851c6b31d8a4d42fd7d3e47b20861b025f2 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0xf6c4e4f339912541d3f8ed99dba64a1372af5e5b - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/celo/0x9263bb7e2d3570593d80d087ea2cfc72882cfb2c + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0x4f122edcd91af8cda38c3a87158afa8687bab57c - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/celo/0x357596dd7a0ef5cb703c5aae4da01edff176ae95 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0xed3fe08bd12f24dad0f1a1e58610644debe374fb - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/celo/0x7766bdc5ff15d3aceb4d37914963aebaccf3de15 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0x5016cd7b785a773f7f3a3ff4035a1e7a76543946 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/celo/0xc973c86afc23ed731ce1a14d7179003a1601205f + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0xf6c4e4f339912541d3f8ed99dba64a1372af5e5b - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/celo/0x52c77b0cb827afbad022e6d6caf2c44452edbc39 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0x7bc815ca2c2115f896bb14b31b8196388c05e99b - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/celo/0xe46935ae80e05cdebd4a4008b6ccaa36d2845370 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0x5016cd7b785a773f7f3a3ff4035a1e7a76543946 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/celo/0xc555d55279023e732ccd32d812114caf5838fd46 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0xf6c4e4f339912541d3f8ed99dba64a1372af5e5b - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/celo/0x3041cbd36888becc7bbcbc0045e3b1f144466f5f + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0xd29c2df656b2e4ae6b6817ccc2ebe932fc6a950b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0x1f6082db7c8f4b199e17090cd5c8831a1dad1997 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0x5016cd7b785a773f7f3a3ff4035a1e7a76543946 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0xf6c4e4f339912541d3f8ed99dba64a1372af5e5b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0xc64f886397988ff16d72123dbe3d46e5bf33ffac - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x0d2c430c6f7ef48ed34bf4aad0ec377e03cc53cf - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x5016cd7b785a773f7f3a3ff4035a1e7a76543946 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0xf6c4e4f339912541d3f8ed99dba64a1372af5e5b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x2b11a34f52e354ef197f0a2397008699b875ae7e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x5016cd7b785a773f7f3a3ff4035a1e7a76543946 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0xf6c4e4f339912541d3f8ed99dba64a1372af5e5b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0xde27bdec962a74a72fa1c5ef50bff6f3da083e05 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0x7766bdc5ff15d3aceb4d37914963aebaccf3de15 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0x5016cd7b785a773f7f3a3ff4035a1e7a76543946 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/celo/0x7924a818013f39cf800f5589ff1f1f0def54f31f + 2025-03-20T21:18:53.078Z 0.8 https://app.uniswap.org/explore/pools/celo/0xf6c4e4f339912541d3f8ed99dba64a1372af5e5b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x67ab7dc903a10838a0de8861dfdff3287cf98e5c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0x88aaeed1fcfca2eda30749afa9ad45a75c80e292 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x2c8e9a1586ed822f79c0a241e1a4d48e839b3182 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x847165954680b989902e354f34d08b09afab3cd9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0x590269935821d760c54b32d31db66ba47d4e53b4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0x03d70bf9e6afbf8cac09ef0c45f9a00a841c2bed - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x8b238f615c1f312d22a65762bcf601a37f1eeec7 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x5280d5e63b416277d0f81fae54bb1e0444cabdaa - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0xf4e43a4a17d2820c7cf724e46844943931a47894 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x5ab53ee1d50eef2c1dd3d5402789cd27bb52c1bb - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0xe6ff8b9a37b0fab776134636d9981aa778c4e718 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x44af8d03393e498eec5fcfc7936ebc381f02974d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x4094915f7849b26e8d43dee1f7e3b7b477a0b5bb - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0xc3f5e0d4cdff86e85486cf6bd20cc0884df5f98e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x87428a53e14d24ab19c6ca4939b4df93b8996ca9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x9dbe5dffaeb4ac2e0ac14f8b4e08b3bc55de5232 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0xc3576f38c32e95e36bbd8d91e6cbe646a3723110 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x8d58e202016122aae65be55694dbce1b810b4072 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0xc3576f38c32e95e36bbd8d91e6cbe646a3723110 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x8d58e202016122aae65be55694dbce1b810b4072 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0xa7bb0d95c6ba0ed0aca70c503b34bc7108589a47 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0xbcfac19a0036ada56496316ee5cf388c2af2bf58 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0x296b88b607ea3a03c821ca4dc34dd9e7e4efa041 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0xc3576f38c32e95e36bbd8d91e6cbe646a3723110 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0x8d58e202016122aae65be55694dbce1b810b4072 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0x019c29d5c97f8cbaa67013e2cf4b6506a5cf183a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0xc3576f38c32e95e36bbd8d91e6cbe646a3723110 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0x8d58e202016122aae65be55694dbce1b810b4072 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x30442fcebbd75a5bb58377c0174d5ce637e297d7 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x6c561b446416e1a00e8e93e221854d6ea4171372 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x0fb597d6cfe5be0d5258a7f017599c2a4ece34c7 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0xe9b7057f9b81a0120c09306d35f22859473f18cb - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x8deb37b048f4b3c7bd61eca7dfccbef7cba726de - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x455fd3ae52a8ab80f319a1bf912457aa8296695a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0xe11d03bef391ee0a4b670176e23eb44aad490f12 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0xe7f850731fed6af4c36cce93eccfbcda0634a030 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0xadad4ce0c68f50a19cf5063e0b91d701daab1df1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x5e9bb3d7682a9537db831060176c4247ab80d1ec - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0xe9ed60539a8ea7a4da04ebfa524e631b1fd48525 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x0511791eb6fb175a1aaa645114f0f5c8689ec163 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0xf3c7b93db3f28580b0fd10365e619eedceb40e76 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x58ecf9cec06bc58fde9280d348f79ed8f3d3046e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0xedc7f0dfd9751ef95bb8786a3b130f490743bb0e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0xc3576f38c32e95e36bbd8d91e6cbe646a3723110 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x8d58e202016122aae65be55694dbce1b810b4072 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x6bcb0ba386e9de0c29006e46b2f01f047ca1806e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0xc3576f38c32e95e36bbd8d91e6cbe646a3723110 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x8d58e202016122aae65be55694dbce1b810b4072 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0xc3576f38c32e95e36bbd8d91e6cbe646a3723110 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0x8d58e202016122aae65be55694dbce1b810b4072 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x34a43471377dcce420ce8e3ffd9360b2e08fa7b4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x34a43471377dcce420ce8e3ffd9360b2e08fa7b4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0x34a43471377dcce420ce8e3ffd9360b2e08fa7b4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0x34a43471377dcce420ce8e3ffd9360b2e08fa7b4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x34a43471377dcce420ce8e3ffd9360b2e08fa7b4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x34a43471377dcce420ce8e3ffd9360b2e08fa7b4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0x34a43471377dcce420ce8e3ffd9360b2e08fa7b4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0x766854992bd5363ebeeff0113f5a5795796befab - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0x9438a9d1bdeece02ed4431ac59613a128201e0b9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0x0a63d3910ffc1529190e80e10855c4216407cc45 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x89084692453ab2305f5f8ac7d70d5efd37a86b8f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0xb34a5657988da5b9888952c439756594613507aa - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0x05efb437e4e97efea6450321eca8d7585a731369 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0xc973c86afc23ed731ce1a14d7179003a1601205f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0x0f44a1c2b66418f784607d2067fe695703809bff - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x0da6253560822973185297d5f32ff8fa38243afe - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x622270721fb38fde831ab23a8e177665557f6fa9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0xa95b0f5a65a769d82ab4f3e82842e45b8bbaf101 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x622270721fb38fde831ab23a8e177665557f6fa9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0x886b4f0cb357e0d6ec07b7a3985f346cc17ece7d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0x622270721fb38fde831ab23a8e177665557f6fa9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0x50defb73a76efe5d5d35cf267ffb02dfd6cd96bc - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0x622270721fb38fde831ab23a8e177665557f6fa9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x7aea2e8a3843516afa07293a10ac8e49906dabd1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x8c7080564b5a792a33ef2fd473fba6364d5495e5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x8f81b80d950e5996346530b76aba2962da5c9edb - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x7bc0f74d8d94e8e9fdaa40bbc04cc44fb8e0f081 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x09c149c856e6fb6e40aa39209142411b554b1a41 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x622270721fb38fde831ab23a8e177665557f6fa9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x7ef0a523c49b1dd07e3593198c5260a95ad7859a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x622270721fb38fde831ab23a8e177665557f6fa9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0x622270721fb38fde831ab23a8e177665557f6fa9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x15aa01580ae866f9ff4dbe45e06e307941d90c7b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x4548280ac92507c9092a511c7396cbea78fa9e49 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0xe0554a476a092703abdb3ef35c80e0d76d32939f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0xc555d55279023e732ccd32d812114caf5838fd46 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x421803da50d3932caa36bd1731d36a0e2af93542 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0xc555d55279023e732ccd32d812114caf5838fd46 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0xc555d55279023e732ccd32d812114caf5838fd46 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0x258a4b7373f6863db5a17de191e0cebb1e0bbc8a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0x45126b956401daaec92afba2a9953e14b16fb83f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0xc555d55279023e732ccd32d812114caf5838fd46 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0xa3eaa52b505cf61aadcfe21424d43a6847dd6331 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x722bcf6c16dadcc29914e4e64290c46aa1406de8 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x1e1367dcebe168554e82552e0e659a4116926d10 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0xc555d55279023e732ccd32d812114caf5838fd46 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x4d170f8714367c44787ae98259ce8adb72240067 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0xc555d55279023e732ccd32d812114caf5838fd46 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0xaa97f0689660ea15b7d6f84f2e5250b63f2b381a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0xc555d55279023e732ccd32d812114caf5838fd46 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0xb736330326cf379ecd918dba10614bd63c2713da - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0xe3d4faff3179f0a664a3a84c3e1da3b90e27f186 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x50e7b9293aef80c304234e86c84a01be8401c530 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0x87dddd2e152bf1955e7e03d9f23a9dcc163eebf6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0xd9dd34576c7034beb0b11a99afffc49e91011235 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0x394a9fcbab8599437d9ec4e5a4a0eb7cb1fd2f69 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0xb3adde966b8a1a6f22a04914ee9fe0798e71fc5b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0xa2d4a8e00daad32acace1a0dd0905f6aaf57e84e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0x2392ae4ba6daf181ce7343d237b695cdf525e233 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0xc2c390c6cd3c4e6c2b70727d35a45e8a072f18ca - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x95f4408736988549212db071b1c8d20f7c4e6304 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x52c77b0cb827afbad022e6d6caf2c44452edbc39 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x3dd2fdba71282083d440687cce9e4231aaac534e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0xe4d9faddd9bca5d8393bee915dc56e916ab94d27 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x95f4408736988549212db071b1c8d20f7c4e6304 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x52c77b0cb827afbad022e6d6caf2c44452edbc39 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0x9c92ed19a86986124447a73b27625230dd52f805 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0x740601243a6aa25ce4ee2d196eef83ac3bec6c65 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0x95f4408736988549212db071b1c8d20f7c4e6304 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0x52c77b0cb827afbad022e6d6caf2c44452edbc39 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0x95f4408736988549212db071b1c8d20f7c4e6304 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0x52c77b0cb827afbad022e6d6caf2c44452edbc39 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0xafbb6fcc92ddb091dbc13e9073c3360c7d9600cc - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x95f4408736988549212db071b1c8d20f7c4e6304 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x52c77b0cb827afbad022e6d6caf2c44452edbc39 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0xf54eba95d7f8dbe4bfeb0b6e038b3c2bedd3e40a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x05c0a0b84b6b67499c33e6403686f45cab063810 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x9169bf3657353e4b2b81c75e235f22bc299a7780 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x95f4408736988549212db071b1c8d20f7c4e6304 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x52c77b0cb827afbad022e6d6caf2c44452edbc39 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0x95f4408736988549212db071b1c8d20f7c4e6304 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0x52c77b0cb827afbad022e6d6caf2c44452edbc39 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0xb0bb2c1d32c7b27f21eec4402c6d1c38795c090a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x534d3930edba2c0b90a7973549a0287141c987ef - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0xf27d0dac09460b236d4d9e0da316fe9c3a99b4a2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x0c3fdf9c70835f9be9db9585ecb6a1ee3f20a6c7 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0xdd672b3b768a16b9bcb4ee1060d3e8221435beaa - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x0c3fdf9c70835f9be9db9585ecb6a1ee3f20a6c7 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0xa39fe8f7a00ce28b572617d3a0bc1c2b44110e79 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0x0c3fdf9c70835f9be9db9585ecb6a1ee3f20a6c7 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0x0c3fdf9c70835f9be9db9585ecb6a1ee3f20a6c7 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x0c3fdf9c70835f9be9db9585ecb6a1ee3f20a6c7 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x96d5d78b179169ee0a0a0104dc514988f2a797fe - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x0c3fdf9c70835f9be9db9585ecb6a1ee3f20a6c7 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0xb466d5429d6ad9999bf112c225d9d7b15e96c658 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0x0c3fdf9c70835f9be9db9585ecb6a1ee3f20a6c7 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x8a35d2635aeca1aaf667d77ed9ff3b21e48ede24 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0xe566e99d65b17974fd9db02e25e24ea8020f7a0e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x5c3edc45ae71a353c669cfa71e6488951dce4618 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0xa7b3bcc6c88da2856867d29f11c67c3a85634882 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x543842cbfef3b3f5614b2153c28936967218a0e6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0xc45a81bc23a64ea556ab4cdf08a86b61cdceea8b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0xe333e366503f620e0242796431dc74fffd258e66 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x42161084d0672e1d3f26a9b53e653be2084ff19c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0xe24f62341d84d11078188d83ca3be118193d6389 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0x782dcc2cd3a65405baeb794269703e9c29a175cc - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0xae8d5b91fca627410a3bef77f55fcfe208409a40 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0xa42eb1c1a212da9e24058c6afc0ea906fecb8351 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x0e3529cf622dc1141a31cfc0fc85f679f558c92b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x6f5ec7c65c2744a963064f6d49df0f4eea7d7d90 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x9a772018fbd77fcd2d25657e5c547baff3fd7d16 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0xfc1505b3d4cd16bb2336394ad11071638710950f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x8e0a7d4018fb2674346d5742055174f899fe1826 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0xf8aa1db87d84118b0b461e2135190ac27fc1859d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0xe8f7c89c5efa061e340f2d2f206ec78fd8f7e124 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x56534741cd8b152df6d48adf7ac51f75169a83b2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x73a38006d23517a1d383c88929b2014f8835b38b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0xf5d63f66a36be31a106631f276794223b8ce5280 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0xcf0bb95967cd006f5eaa1463c9d710d1e1550a96 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0xdc9bf303e72a5780c45d53fc12799164e5ba8271 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x1d4dab3f27c7f656b6323c1d6ef713b48a8f72f1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0xafd8f9b89e2af8246523573a369010daf9489b12 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0xbd045175d2a1451a015079f5f3f59ca5c05524ea - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x859ec3d336bb5508f6d87fea2d49c9294adae311 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x8544383f6f2eb43711fba8d918b30658856b9806 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x308c6fbd6a14881af333649f17f2fde9cd75e2a6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x704ad8d95c12d7fea531738faa94402725acb035 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x5166c1bd4603cf67dbb9a98940e38d2bd0a7f294 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0xf215cedbae999571e4ba5d80c10b6e835f88d5ec - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x308c6fbd6a14881af333649f17f2fde9cd75e2a6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x704ad8d95c12d7fea531738faa94402725acb035 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0xd4cb5566b5c16ef2f4a08b1438052013171212a2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0x308c6fbd6a14881af333649f17f2fde9cd75e2a6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0x704ad8d95c12d7fea531738faa94402725acb035 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0x357faf5843c7fd7fb4e34fbeabdac16eabe8a5bc - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0x308c6fbd6a14881af333649f17f2fde9cd75e2a6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0x704ad8d95c12d7fea531738faa94402725acb035 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x2bbfb5a2496f405d4094d4b854daeb9ce70d0029 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x308c6fbd6a14881af333649f17f2fde9cd75e2a6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x704ad8d95c12d7fea531738faa94402725acb035 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x3373a22cb07cb49651b82cf6f174ef434e4dbaa8 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0xc8d19b4ea42939a4b14260f0c8b4a0d6f70c8496 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x308c6fbd6a14881af333649f17f2fde9cd75e2a6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x704ad8d95c12d7fea531738faa94402725acb035 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0xb2290db2f409201c33c507d266becabf19228dd1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0x308c6fbd6a14881af333649f17f2fde9cd75e2a6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0x704ad8d95c12d7fea531738faa94402725acb035 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x0c30062368eefb96bf3ade1218e685306b8e89fa - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x6f38e884725a116c9c7fbf208e79fe8828a2595f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0xe612cb2b5644aef0ad3e922bae70a8374c63515f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0xdef705a1864bcba65e4e275bffd58de21b5d44a0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x717358a47ac99f3cd233e723be331756b3951164 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x9166a0139cab9661e08779cd01b1358aaea7b95f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0xc1a6fbedae68e1472dbb91fe29b51f7a0bd44f97 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x3a3dc4a26d1aceae12fd1026a5856f12d20658ea - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0xa2375dad211fe6e538d29c98ec526246e38be4ec - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x4e4a4c4c46d3488ff35ff05a0233785a30f03ec4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x5d27fdd96c8e4028edbabf3d667be24769425199 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x1944ac04bd9fed9a2bcdb38b70c35949c864ec35 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0x5e2cd0da3411449152010d8b7f2b624eb29cca59 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0x85e8d0fddf559a57aac6404e7695142cd53eb808 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0xead1cd21ddf8793debc9484a0b8d286230c9b5a3 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0xc4ecaf115cbce3985748c58dccfc4722fef8247c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x2f42df4af5312b492e9d7f7b2110d9c7bf2d9e4f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x021235b92a4f52c789f43a1b01453c237c265861 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0xc6e291f54532f12391ab59d7af75453db2dd784a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0xfe4fe5b4575c036ac6d5cccfe13660020270e27a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0xfa7d79f971a70771e5e92bd80ab955edc8602f4d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x023b6298e2f9ae728b324757599f2a36e002a55a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x53d3e59faac08184720bcb2816f4cf5b36d6767d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x7e9cb8ad4a7683070e233f3eb1d07d87272b9b26 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0xa213c82265cd3d94f972f735a4f5130e34df81bc - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0xf1fdc83c3a336bdbdc9fb06e318b08eaddc82ff4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x3d9228f1847b07e6b2c8eaaf393d5a4db2dbedc2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0x9263bb7e2d3570593d80d087ea2cfc72882cfb2c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x360b9726186c0f62cc719450685ce70280774dc8 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x9ec9620e1fda9c1e57c46782bc3232903cacb59b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x1112956589a2bea1b038732db4ea6b0c416ef130 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x663b1d43c27e41e5e512bf59010133997d1cd304 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x9ec9620e1fda9c1e57c46782bc3232903cacb59b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x1112956589a2bea1b038732db4ea6b0c416ef130 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0xfea834a5c47b923add607cc5b96288d18ffb9c3f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0x9ec9620e1fda9c1e57c46782bc3232903cacb59b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0x1112956589a2bea1b038732db4ea6b0c416ef130 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0x273d580e9ceadca5b2a8ceb5ebb38a70511377cb - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0x9ec9620e1fda9c1e57c46782bc3232903cacb59b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0x1112956589a2bea1b038732db4ea6b0c416ef130 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x3b241fb91c65f42432ebdbca029e0b511c8a1707 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0xe9a65059e895dd5d49806f6a71b63fed0ffffd4b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x01a1f5758c3a53057b6c819ec7331e39c167794a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x40fc7cda03139ebf7a0d3fc01f12b9d9a878cc92 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x11e26bbd1a5547895a50fc39a2d4c0025dec0bda - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x21cbb0e695b0ac79be756a87da690fd80bef4bff - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x68b27e9066d3aadc6078e17c8611b37868f96a1d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x9ec9620e1fda9c1e57c46782bc3232903cacb59b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x1112956589a2bea1b038732db4ea6b0c416ef130 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x2af64d33a47e7a98eafc20ce9f6af59927d10260 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x9ec9620e1fda9c1e57c46782bc3232903cacb59b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x1112956589a2bea1b038732db4ea6b0c416ef130 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0x9ec9620e1fda9c1e57c46782bc3232903cacb59b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0x1112956589a2bea1b038732db4ea6b0c416ef130 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0xc4ce8e63921b8b6cbdb8fcb6bd64cc701fb926f2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x9febc984504356225405e26833608b17719c82ae - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x59c38b6775ded821f010dbd30ecabdcf84e04756 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x2aeee741fa1e21120a21e57db9ee545428e683c9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x67324985b5014b36b960273353deb3d96f2f18c2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x2aeee741fa1e21120a21e57db9ee545428e683c9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x67324985b5014b36b960273353deb3d96f2f18c2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0x2aeee741fa1e21120a21e57db9ee545428e683c9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0x67324985b5014b36b960273353deb3d96f2f18c2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0xcec31e540163ddf45a394e00b11ae442ddc0d704 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0x7f9121b4f4e040fd066e9dc5c250cf9b4338d5bc - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0x2aeee741fa1e21120a21e57db9ee545428e683c9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0x67324985b5014b36b960273353deb3d96f2f18c2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x316f12517630903035a0e0b4d6e617593ee432ba - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x61928bf5f2895b682ecc9b13957aa5a5fe040cc0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x9399da51c1a85e64cce4b30b554875d2b89b2445 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x0962a51e121aa8371cd4bb0458b7e5a08c1cbd29 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0xfbb6eed8e7aa03b138556eedaf5d271a5e1e43ef - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x2aeee741fa1e21120a21e57db9ee545428e683c9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x67324985b5014b36b960273353deb3d96f2f18c2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x913a4ed1636c474e6451b5e9249d94046a24bb33 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x8e3ecc0b261f1a4db62321090575eb299844f077 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x2aeee741fa1e21120a21e57db9ee545428e683c9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x67324985b5014b36b960273353deb3d96f2f18c2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0x2aeee741fa1e21120a21e57db9ee545428e683c9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0x67324985b5014b36b960273353deb3d96f2f18c2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x99950bae3d0b79b8bee86a8a208ae1b087b9dcb0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x948b54a93f5ad1df6b8bff6dc249d99ca2eca052 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x5738df8073ad05d0c0fcf60e358033268ebf16cc - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0xb771f724c504b329623b0ce9199907137670600e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x0da7096f14303eddd634c0241963c064e0244984 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x52f9d14bed8ce6536da063aaf274ae2747ef4853 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0xc0f05732d1cda6f59487ceeef4390abcad86ea3e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0xb771f724c504b329623b0ce9199907137670600e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x0da7096f14303eddd634c0241963c064e0244984 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0x28117b7b8dba890041d7ebe646082af043533da2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0xb771f724c504b329623b0ce9199907137670600e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0x0da7096f14303eddd634c0241963c064e0244984 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0xf3ca4ade682c5b99507db9a72549318b8708f137 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0xb771f724c504b329623b0ce9199907137670600e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0x0da7096f14303eddd634c0241963c064e0244984 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x9c087eb773291e50cf6c6a90ef0f4500e349b903 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x037818b04ac34ea8b54b6683b79ef24d23c0e7cb - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0xbf6ef625de5df898cc1d0f91868aae03976a2e2d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0xfdbaf04326acc24e3d1788333826b71e3291863a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0xc68b994e2147b8bcf18f82c201ac3ee1e97be33d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x47808ddbc91646b21b307fefbaf7ee200b004ccc - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0xb771f724c504b329623b0ce9199907137670600e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x0da7096f14303eddd634c0241963c064e0244984 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0xafa5421fe7997c16e11458659f5a87d67f1e8651 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x26b4103c8da21725909955fe85f7f6249d05dd9e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x79dbd26d3c1e44171205f258aadfae84933b69b8 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0xb771f724c504b329623b0ce9199907137670600e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x0da7096f14303eddd634c0241963c064e0244984 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0xb771f724c504b329623b0ce9199907137670600e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0x0da7096f14303eddd634c0241963c064e0244984 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x0f9e3c4b905e6292b33f5ef96af18054ded12ac8 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0xb0e1a214130245d289ae425db7826576694a5b5f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x32a5746ba6826828716cc1a394bc33301ebc7656 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x0f725b113979e025c69da0ffce3fbf5b6063cc5c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x056c5ff8380625cc94efe865a4c178a33ed546f8 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0xda67d7c01c4c8f757c105c0890d94ac489952cd5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x4fe87203b27a105a772f195d3f30dea714d1ecf0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x670e77c361375be9013869ccc516027ccc90383f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0xaec085e5a5ce8d96a7bdd3eb3a62445d4f6ce703 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x92d90f7f8413749bd4bea26dde4e29efc9e9a0b6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x2c114787d52e9f080464dbed8e285e07ec4e120f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x303b00d7a2ad12a480db7c04de5835ec9ccc37b0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x6f9d09253f99d2b6843b5ec62c23496c37327216 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x4cd15f2bc9533bf6fac4ae33c649f138cb601935 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x63d134fa19fbb35ad689dbb6b659879de1e7fb29 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x5cbddc44f31067df328aa7a8da03aca6f2edd2ad - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0x72da5b3c28b8cee48158f469e0e9215607fe06d7 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0xa7cca1c4a7d4b70f687380e0454e5ae418db53b1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0xd364eb55e17700b54bd75feb3f14582ed7a29444 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0xb3fb7ccf7b681e9562c6da467db4859a8ef0b8de - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x0eb7fbe43045426938ddadc11dc41338e0907659 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0xa23fab21d0653c231166b31cb6274ff45eba2ee5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0xf68001b66cb98345c05b2e3efdee1db8fc01a76c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0xa3adcaaf941a01eec47655c550dd5504637d029a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x6dcba3657ee750a51a13a235b4ed081317da3066 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0xbbf3209130df7d19356d72eb8a193e2d9ec5c234 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x6de5072c06cbf37da96ccc0fc85c85ca82fe9d13 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x183ea22691c54806fe96555436dd312b6befac2f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x6f169193b181d9492f9bde038109cce6dfe19321 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0xe9d448cc1ed83891ab5b381face53f0cadb3a8e5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0xcbfb0745b8489973bf7b334d54fdbd573df7ef3c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0xa04d13f092f68f603a193832222898b0d9f52c71 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x0df407bc6abe9af2093dcb4c974e18d40a6a381a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0xb2c5d104f481d0beb056842bd5312be6fd831429 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x0b07188b12e3bba6a680e553e23c4079e98a034b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0xb2c5d104f481d0beb056842bd5312be6fd831429 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x0b07188b12e3bba6a680e553e23c4079e98a034b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0xb2c5d104f481d0beb056842bd5312be6fd831429 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0x0b07188b12e3bba6a680e553e23c4079e98a034b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0x56fcb902bee19a645f9607cd1e1c0737b6358feb - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0xb2c5d104f481d0beb056842bd5312be6fd831429 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0x0b07188b12e3bba6a680e553e23c4079e98a034b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x7b9fda92bfa6fdadfdc4f6c72c0cc8336e7d7497 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x1d2bdb7117a5a7d7fe4c1d95681a92e4df13bb69 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0xb2c5d104f481d0beb056842bd5312be6fd831429 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x0b07188b12e3bba6a680e553e23c4079e98a034b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x7e4fb9e08cf122feb925117bace017ea234944d3 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0xb2c5d104f481d0beb056842bd5312be6fd831429 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x0b07188b12e3bba6a680e553e23c4079e98a034b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0xb2c5d104f481d0beb056842bd5312be6fd831429 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0x0b07188b12e3bba6a680e553e23c4079e98a034b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x867b321132b18b5bf3775c0d9040d1872979422e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0xb26a868ffa4cbba926970d7ae9c6a36d088ee38c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0xc4329493d7566525a4d51698a33f43ad240e9290 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0x96239bd7ae3d9bc253b1cc7cf7a84f3a67ca5369 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x2ff525c71cb7b29fcde4bea8c8f601b1dd22480a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x43f34a518e20b9454c94bf4026ec9024ed84a062 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x12146c8e7469be19ec6c7f58b80246548144f8b8 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x026428e531f30b2714ceff3781d7cdf5d278e96a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x2049df3435bdbb36d22f98fcd2e5027049a1f3ce - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x2049df3435bdbb36d22f98fcd2e5027049a1f3ce - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0x2049df3435bdbb36d22f98fcd2e5027049a1f3ce - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0x22d1c3c541dd649ea4a8709fc787f348dc069e95 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0xd9abecb39a5885d1e531ed3599adfed620e2fc8a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0x25fb97799f80433e422f47e75173314e54dae174 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0x2049df3435bdbb36d22f98fcd2e5027049a1f3ce - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x2049df3435bdbb36d22f98fcd2e5027049a1f3ce - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x3e2e284d55926f5f6e86987da6be216aef292e76 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x2049df3435bdbb36d22f98fcd2e5027049a1f3ce - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0x2049df3435bdbb36d22f98fcd2e5027049a1f3ce - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0xc8219b876753a85025156b22176c2edea17aac53 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x27807dd7adf218e1f4d885d54ed51c70efb9de50 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x1d69099803b4580efb8df0c7ef083e550a1c42c1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0xec558e484cc9f2210714e345298fdc53b253c27d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x37bb450b17721c6720040a150029e504766e9777 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x4cd04970dcad72d09c3af2e09f15bbcd2eb1d5d4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x96aa22baedc5a605357e0b9ae20ab6b10a472e03 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x96aa22baedc5a605357e0b9ae20ab6b10a472e03 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0x96aa22baedc5a605357e0b9ae20ab6b10a472e03 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0xc9d0cae8343a2231b1647ab00e639eabdc766147 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0x96aa22baedc5a605357e0b9ae20ab6b10a472e03 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x8c7adf7bcfdfca0a27f3d7ad49698b9e11c1f20b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x96aa22baedc5a605357e0b9ae20ab6b10a472e03 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x98e4799d58982ed714159c30a34b4bdb20ba20b7 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x96aa22baedc5a605357e0b9ae20ab6b10a472e03 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0x96aa22baedc5a605357e0b9ae20ab6b10a472e03 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x8ff82952cee74c095c1734ee144143a755d3c600 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0xf7513c120b92fa4dd5cbfa78dffefcb4ced5743f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0xdd2e0d86a45e4ef9bd490c2809e6405720cc357c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x6ec94f50cadcc79984463688de42a0ca696ec2db - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x6ec94f50cadcc79984463688de42a0ca696ec2db - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0x6ec94f50cadcc79984463688de42a0ca696ec2db - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0x6ec94f50cadcc79984463688de42a0ca696ec2db - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0xd2fdfd5059a83e15bf362f094a2ae63f03b554ca - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x5116773e18a9c7bb03ebb961b38678e45e238923 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x6ec94f50cadcc79984463688de42a0ca696ec2db - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0xbac01354f2109eb3aebcb46b3ee43813dbae1a7d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x6ec94f50cadcc79984463688de42a0ca696ec2db - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0x357596dd7a0ef5cb703c5aae4da01edff176ae95 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/celo/0x6ec94f50cadcc79984463688de42a0ca696ec2db - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0xf8e349d1d827a6edf17ee673664cfad4ca78c533 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x48da0965ab2d2cbf1c17c09cfb5cbe67ad5b1406 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x49d27c3e1cac5bcf7615f2f8e1c1af6ab9db8225 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0x679420c54cc4806d0f480925772965746d9f9779 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x20b795a5bb2cca7598b67739dba8666ab3c506f8 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x55703b183e4676d3e72289995c2e14fa0cc29c1b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/bnb/0x3e7995110680e6a55e6e430a1c511e921f896316 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0xd19a1c8278d0420bfb2c825d99dc31ff86224607 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x9c42751954513c0461481a9600c9d11a059ddd12 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/base/0x7d68d2a3b22824a7895ad475f4fc3ca9ac8a240a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0x65081cb48d74a32e9ccfed75164b8c09972dbcf1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0x765c3773f641ad8073795765c5c41c075f91b140 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0x18b268965e4e702bdf13469205937894b8ab0ee8 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0x6b918c9f87b46a758c2b51bce427c8028dacb720 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0xbda709a0665b340898856b8b29ff87079bb130d3 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0xd49174dba635489c67fa628864c2d0d04824ebd8 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0x9e2ef5522b2f9eac00912f25082f6e652123b54d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0xc673d5164103357a7537c36438a6326776a14bbd - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0xe97a0889d2b0660fddc144a8893b3ac9236756a6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0x12095933f1eeb066176dd2e41e5a2f8be6974616 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0x8eb4b07affbd1083f42032eed35cd32e382ee8b7 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0xfbc45ab96d02e150b2ddeb7dd4eacd3d8c674f4a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0x9d7151413833dcb13ae284d6a7fccd93989c47a1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0x18ce92e7a37d994657f97c3defaf880a805f08d5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0xa9c6669de2c04c2adb22ac7a65d75b47fee30e35 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0x6e4e17973cb963c9931617d8d0e35813cb5eb886 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0x8927058918e3cff6f55efe45a58db1be1f069e49 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0x21b8065d10f73ee2e260e5b47d3344d3ced7596e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0xa43fe16908251ee70ef74718545e4fe6c5ccec9f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0x9c4fe5ffd9a9fc5678cfbd93aa2d4fd684b67c4c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0x0d4a11d5eeaac28ec3f61d100daf4d40471f1852 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0xbb2b8038a1640196fbe3e38816f3e67cba72d940 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0x7b73644935b8e68019ac6356c40661e1bc315860 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0xca7c2771d248dcbe09eabe0ce57a62e18da178c0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0xa478c2975ab1ea89e8196811f51a7b7ade33eb11 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0x52c77b0cb827afbad022e6d6caf2c44452edbc39 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0xc2eab7d33d3cb97692ecb231a5d0e4a649cb539d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0x180efc1349a69390ade25667487a826164c9c6e4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0x43de4318b6eb91a7cf37975dbb574396a7b5b5c6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0x9ec9367b8c4dd45ec8e7b800b1f719251053ad60 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0x25647e01bd0967c1b9599fa3521939871d1d0888 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0x3041cbd36888becc7bbcbc0045e3b1f144466f5f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0xe46935ae80e05cdebd4a4008b6ccaa36d2845370 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0x09d1d767edf8fa23a64c51fa559e0688e526812f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0xc555d55279023e732ccd32d812114caf5838fd46 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0x48d20b3e529fb3dd7d91293f80638df582ab2daa - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0x2a6c340bcbb0a79d3deecd3bc5cbc2605ea9259f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0xcaa004418eb42cdf00cb057b7c9e28f0ffd840a5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0x1ffec7119e315b15852557f654ae0052f76e6ae1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0x8c1c499b1796d7f3c2521ac37186b52de024e58c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0x4028daac072e492d34a3afdbef0ba7e35d8b55c4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0xd3d2e2692501a5c9ca623199d38826e513033a17 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0x6ada49aeccf6e556bb7a35ef0119cc8ca795294a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0x9ff68f61ca5eb0c6606dc517a9d44001e564bb66 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0xddd23787a6b80a794d952f5fb036d0b31a8e6aff - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0x470e8de2ebaef52014a47cb5e6af86884947f08c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0xda2d09fbbf8ee4b5051a0e9b562c5fcb4b393b18 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0xf6c4e4f339912541d3f8ed99dba64a1372af5e5b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0x7924a818013f39cf800f5589ff1f1f0def54f31f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0x69c7bd26512f52bf6f76fab834140d13dda673ca - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0x2caccf71bdf8fff97c06a46eca29b611b1a74b5e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0xaf996125e98b5804c00ffdb4f7ff386307c99a00 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0x0f23d49bc92ec52ff591d091b3e16c937034496e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0x0b07188b12e3bba6a680e553e23c4079e98a034b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0xb771f724c504b329623b0ce9199907137670600e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0xc7e6b676bfc73ae40bcc4577f22aab1682c691c6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0x6ec94f50cadcc79984463688de42a0ca696ec2db - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0xaf21b0ec0197e63a5c6cc30c8e947eb8165c6212 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0x5ced44f03ff443bbe14d8ea23bc24425fb89e3ed - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0xc5be99a02c6857f9eac67bbce58df5572498f40c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0xc91ef786fbf6d62858262c82c63de45085dea659 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0x97e1fcb93ae7267dbafad23f7b9afaa08264cfd8 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0xa1bf0e900fb272089c9fd299ea14bfccb1d1c2c0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0xa5e9c917b4b821e4e0a5bbefce078ab6540d6b5e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0x0da7096f14303eddd634c0241963c064e0244984 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/unichain/0x96aa22baedc5a605357e0b9ae20ab6b10a472e03 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0xfae3f424a0a47706811521e3ee268f00cfb5c45e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x7b602f98d71715916e7c963f51bfebc754ade2d0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0xb978a8c502ce97b04043036a91548b846067f9ea - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x804226ca4edb38e7ef56d16d16e92dc3223347a0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x9ba9c677d19347abfba1d6b6d6ceb61942071561 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x5e8754aaa7c2da42c218f40435cbdedbae88bfc5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0xeb7e0191f4054868d97f33ca7a4176b226ccbd2f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0xd1356d360f37932059e5b89b7992692aa234eda6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x27b571f3e7f7827b13d927d2d59244e3e58a7d1a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0xb2dc1235bf4b36628a8665aeb668bf202759528a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x2e587b9e7aa638d7eb7db5fe7447513bc4d0d28b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x724f6a02ed2eb82d8d45034b280903cf663731ab - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0xe54dadf5b4f8779256e1bbb94eca00b124311208 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0xa7141c79d3d4a9ad67ba95d2b97fe7eed9fb92b3 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x28f577ed1cb3b0add148d745d9c0a2c0c40cc48d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x12089f58e0e7eadfc7d60cf1b1ed6839a811672a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x3859de09906bed098879cbef34d80817357244c7 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x01ed0722f62a5dd3212b04aed2a2065623a705bb - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x178b99eaa2f6bc97d16f84edbce2e23ae6b8140b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x0e663593657b064e1bae76d28625df5d0ebd4421 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0xd18384f4398bf5415902ad5d87eaa96549fd4f1e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x97bb1788dcd64e17166931b87789cae97c154009 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x83b9a48793f4e5be12cfe1de3b1d55a83c4f1a9a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x6d61ae1b0d94941dc702581daaaafc7665d1c6d7 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x9a68f0bbced6d6f1f63f0a61215742377ac9d325 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x01c7c6066ec10b1cd4821e13b9fb063680ffa083 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0xcea3c61705b68fefd8da1048ca69db1344180fcb - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0xf39f633a18042105454ff64c6abd07beb8cbcead - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x7fd0055020a6d9c43c31c8ea755c5038ebd39722 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0xe1e870fdff3ad67f2879542d841d8ab3e1406f4c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0xc0a57a72cb7da55da2e50f664b1641570941618f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0xfd584fcc3a74429d85c9a2294eaf0f566ddfd593 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0xfc1e7bc60adfc151565c033521ee6ccbde027f54 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x78b4c0301b7e6e3d31f259b3f112ced639f030a3 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x94a0d838379e9703b69a777191d8a8951ada2e8e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0xd399718256c5206d9586e866169dbdf131193e02 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x43fb9c3fd6715e872272b0caab968a97692726eb - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x9d1bcb75a7bc5defe7daed505c462572b5e022f9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0xd312a35320a5936409edfd5e2eb0d17bcf99910b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x118bc22a76a71ff3187577422af2d57210277bab - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x90fbfd28f16fa163b914be6b9b3eeeb1c3e02fe5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x44f904512639f2a4530dc3b0cce8927b09226a43 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0xaf4ceda77ec614099ffe67b7f4fe4427d52e75cc - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0xd494b33229ee2c857a43b94ee7117be05e5fd88c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x0e7a5a8177d5711dc90ba9a6ded590060f52dd29 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0xcec63fa2b71744e1cdd48f71e34acedd46b496aa - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x0fbb556c73226b12cd7cfb2bed56e77fc177f0ec - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x15309a0e8c954c46601d8028153125c3b92884b6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0xdbd8a9c9d060973557f3ac7c7f642a9523529d68 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0xc58a6a266ede99e0b8069f3f45a3028c2aaf4c0c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x21b8065d10f73ee2e260e5b47d3344d3ced7596e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0xa43fe16908251ee70ef74718545e4fe6c5ccec9f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x9c4fe5ffd9a9fc5678cfbd93aa2d4fd684b67c4c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x0d4a11d5eeaac28ec3f61d100daf4d40471f1852 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0xbb2b8038a1640196fbe3e38816f3e67cba72d940 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x7b73644935b8e68019ac6356c40661e1bc315860 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0xca7c2771d248dcbe09eabe0ce57a62e18da178c0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0xa478c2975ab1ea89e8196811f51a7b7ade33eb11 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x52c77b0cb827afbad022e6d6caf2c44452edbc39 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0xc2eab7d33d3cb97692ecb231a5d0e4a649cb539d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x180efc1349a69390ade25667487a826164c9c6e4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x43de4318b6eb91a7cf37975dbb574396a7b5b5c6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x9ec9367b8c4dd45ec8e7b800b1f719251053ad60 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x25647e01bd0967c1b9599fa3521939871d1d0888 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x3041cbd36888becc7bbcbc0045e3b1f144466f5f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0xe46935ae80e05cdebd4a4008b6ccaa36d2845370 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x09d1d767edf8fa23a64c51fa559e0688e526812f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0xc555d55279023e732ccd32d812114caf5838fd46 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x48d20b3e529fb3dd7d91293f80638df582ab2daa - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x2a6c340bcbb0a79d3deecd3bc5cbc2605ea9259f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0xcaa004418eb42cdf00cb057b7c9e28f0ffd840a5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x1ffec7119e315b15852557f654ae0052f76e6ae1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x8c1c499b1796d7f3c2521ac37186b52de024e58c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x4028daac072e492d34a3afdbef0ba7e35d8b55c4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0xd3d2e2692501a5c9ca623199d38826e513033a17 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x6ada49aeccf6e556bb7a35ef0119cc8ca795294a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x9ff68f61ca5eb0c6606dc517a9d44001e564bb66 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0xddd23787a6b80a794d952f5fb036d0b31a8e6aff - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x470e8de2ebaef52014a47cb5e6af86884947f08c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0xda2d09fbbf8ee4b5051a0e9b562c5fcb4b393b18 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0xf6c4e4f339912541d3f8ed99dba64a1372af5e5b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x7924a818013f39cf800f5589ff1f1f0def54f31f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x69c7bd26512f52bf6f76fab834140d13dda673ca - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x2caccf71bdf8fff97c06a46eca29b611b1a74b5e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0xaf996125e98b5804c00ffdb4f7ff386307c99a00 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x0f23d49bc92ec52ff591d091b3e16c937034496e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x0b07188b12e3bba6a680e553e23c4079e98a034b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0xb771f724c504b329623b0ce9199907137670600e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0xc7e6b676bfc73ae40bcc4577f22aab1682c691c6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x6ec94f50cadcc79984463688de42a0ca696ec2db - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0xaf21b0ec0197e63a5c6cc30c8e947eb8165c6212 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x5ced44f03ff443bbe14d8ea23bc24425fb89e3ed - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0xc5be99a02c6857f9eac67bbce58df5572498f40c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0xc91ef786fbf6d62858262c82c63de45085dea659 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x97e1fcb93ae7267dbafad23f7b9afaa08264cfd8 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0xa1bf0e900fb272089c9fd299ea14bfccb1d1c2c0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0xa5e9c917b4b821e4e0a5bbefce078ab6540d6b5e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x0da7096f14303eddd634c0241963c064e0244984 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/avalanche/0x96aa22baedc5a605357e0b9ae20ab6b10a472e03 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0x85287626e78602d0da569332e419154b0bdea035 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0xf52b4b69123cbcf07798ae8265642793b2e8990c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0x1cf4cfc7a984a474ab03f444ccedb30c3ae6f56c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0xf5a23bdd36a56ede75d503f6f643d5eaf25b1a8f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0x57529f9e2a23cc53fc387b162d2ab0f1df3ed701 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0xf4b646bc85458cc74497c773e2bc8b9ec1351e97 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0xf08ff66b8ec0db053711f4989c40b084564f7de3 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0x0000fb2a9a0f3f35d72d7eedb8689c6db1d30225 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0xbb60bb410182d8e96c41dfc92e017dd79f5100bf - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0x0f6c5fdcb927b99b3040c609bef07bdfc59a6173 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0xaeba85e3328d6b77b58130f43815ac9c59603d38 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0xbf796b95d09729815806dd50de07c1111aa3926f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0x63f59866a7e9a8628e7f1577ba55da134d64d8c2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0x78337095c61035056a85b7d430d6e9875f177ae2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0x258ba3b253e5cc3bab01c28d2f527aacd6d96793 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0x7b8086f5442f130a6be2d62dff02319018344feb - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0x9dfb11cf311a8fa1296f6958057f8bb02be51a4c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0xf95141f5552e592dc58c41e54d65d0f645ab7d7e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0x21b8065d10f73ee2e260e5b47d3344d3ced7596e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0xa43fe16908251ee70ef74718545e4fe6c5ccec9f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0x9c4fe5ffd9a9fc5678cfbd93aa2d4fd684b67c4c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0x0d4a11d5eeaac28ec3f61d100daf4d40471f1852 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0xbb2b8038a1640196fbe3e38816f3e67cba72d940 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0x7b73644935b8e68019ac6356c40661e1bc315860 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0xca7c2771d248dcbe09eabe0ce57a62e18da178c0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0xa478c2975ab1ea89e8196811f51a7b7ade33eb11 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0x52c77b0cb827afbad022e6d6caf2c44452edbc39 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0xc2eab7d33d3cb97692ecb231a5d0e4a649cb539d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0x180efc1349a69390ade25667487a826164c9c6e4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0x43de4318b6eb91a7cf37975dbb574396a7b5b5c6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0x9ec9367b8c4dd45ec8e7b800b1f719251053ad60 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0x25647e01bd0967c1b9599fa3521939871d1d0888 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0x3041cbd36888becc7bbcbc0045e3b1f144466f5f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0xe46935ae80e05cdebd4a4008b6ccaa36d2845370 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0x09d1d767edf8fa23a64c51fa559e0688e526812f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0xc555d55279023e732ccd32d812114caf5838fd46 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0x48d20b3e529fb3dd7d91293f80638df582ab2daa - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0x2a6c340bcbb0a79d3deecd3bc5cbc2605ea9259f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0xcaa004418eb42cdf00cb057b7c9e28f0ffd840a5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0x1ffec7119e315b15852557f654ae0052f76e6ae1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0x8c1c499b1796d7f3c2521ac37186b52de024e58c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0x4028daac072e492d34a3afdbef0ba7e35d8b55c4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0xd3d2e2692501a5c9ca623199d38826e513033a17 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0x6ada49aeccf6e556bb7a35ef0119cc8ca795294a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0x9ff68f61ca5eb0c6606dc517a9d44001e564bb66 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0xddd23787a6b80a794d952f5fb036d0b31a8e6aff - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0x470e8de2ebaef52014a47cb5e6af86884947f08c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0xda2d09fbbf8ee4b5051a0e9b562c5fcb4b393b18 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0xf6c4e4f339912541d3f8ed99dba64a1372af5e5b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0x7924a818013f39cf800f5589ff1f1f0def54f31f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0x69c7bd26512f52bf6f76fab834140d13dda673ca - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0x2caccf71bdf8fff97c06a46eca29b611b1a74b5e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0xaf996125e98b5804c00ffdb4f7ff386307c99a00 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0x0f23d49bc92ec52ff591d091b3e16c937034496e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0x0b07188b12e3bba6a680e553e23c4079e98a034b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0xb771f724c504b329623b0ce9199907137670600e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0xc7e6b676bfc73ae40bcc4577f22aab1682c691c6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0x6ec94f50cadcc79984463688de42a0ca696ec2db - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0xaf21b0ec0197e63a5c6cc30c8e947eb8165c6212 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0x5ced44f03ff443bbe14d8ea23bc24425fb89e3ed - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0xc5be99a02c6857f9eac67bbce58df5572498f40c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0xc91ef786fbf6d62858262c82c63de45085dea659 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0x97e1fcb93ae7267dbafad23f7b9afaa08264cfd8 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0xa1bf0e900fb272089c9fd299ea14bfccb1d1c2c0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0xa5e9c917b4b821e4e0a5bbefce078ab6540d6b5e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0x0da7096f14303eddd634c0241963c064e0244984 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/blast/0x96aa22baedc5a605357e0b9ae20ab6b10a472e03 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0x21b8065d10f73ee2e260e5b47d3344d3ced7596e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0xa43fe16908251ee70ef74718545e4fe6c5ccec9f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0x9c4fe5ffd9a9fc5678cfbd93aa2d4fd684b67c4c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0x0d4a11d5eeaac28ec3f61d100daf4d40471f1852 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0xbb2b8038a1640196fbe3e38816f3e67cba72d940 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0x7b73644935b8e68019ac6356c40661e1bc315860 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0xca7c2771d248dcbe09eabe0ce57a62e18da178c0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0xa478c2975ab1ea89e8196811f51a7b7ade33eb11 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0x52c77b0cb827afbad022e6d6caf2c44452edbc39 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0xc2eab7d33d3cb97692ecb231a5d0e4a649cb539d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0x180efc1349a69390ade25667487a826164c9c6e4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0x43de4318b6eb91a7cf37975dbb574396a7b5b5c6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0x9ec9367b8c4dd45ec8e7b800b1f719251053ad60 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0x25647e01bd0967c1b9599fa3521939871d1d0888 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0x3041cbd36888becc7bbcbc0045e3b1f144466f5f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0xe46935ae80e05cdebd4a4008b6ccaa36d2845370 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0x09d1d767edf8fa23a64c51fa559e0688e526812f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0xc555d55279023e732ccd32d812114caf5838fd46 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0x48d20b3e529fb3dd7d91293f80638df582ab2daa - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0x2a6c340bcbb0a79d3deecd3bc5cbc2605ea9259f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0xcaa004418eb42cdf00cb057b7c9e28f0ffd840a5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0x1ffec7119e315b15852557f654ae0052f76e6ae1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0x8c1c499b1796d7f3c2521ac37186b52de024e58c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0x4028daac072e492d34a3afdbef0ba7e35d8b55c4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0xd3d2e2692501a5c9ca623199d38826e513033a17 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0x6ada49aeccf6e556bb7a35ef0119cc8ca795294a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0x9ff68f61ca5eb0c6606dc517a9d44001e564bb66 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0xddd23787a6b80a794d952f5fb036d0b31a8e6aff - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0x470e8de2ebaef52014a47cb5e6af86884947f08c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0xda2d09fbbf8ee4b5051a0e9b562c5fcb4b393b18 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0xf6c4e4f339912541d3f8ed99dba64a1372af5e5b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0x7924a818013f39cf800f5589ff1f1f0def54f31f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0x69c7bd26512f52bf6f76fab834140d13dda673ca - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0x2caccf71bdf8fff97c06a46eca29b611b1a74b5e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0xaf996125e98b5804c00ffdb4f7ff386307c99a00 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0x0f23d49bc92ec52ff591d091b3e16c937034496e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0x0b07188b12e3bba6a680e553e23c4079e98a034b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0xb771f724c504b329623b0ce9199907137670600e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0xc7e6b676bfc73ae40bcc4577f22aab1682c691c6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0x6ec94f50cadcc79984463688de42a0ca696ec2db - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0xaf21b0ec0197e63a5c6cc30c8e947eb8165c6212 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0x5ced44f03ff443bbe14d8ea23bc24425fb89e3ed - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0xc5be99a02c6857f9eac67bbce58df5572498f40c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0xc91ef786fbf6d62858262c82c63de45085dea659 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0x97e1fcb93ae7267dbafad23f7b9afaa08264cfd8 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0xa1bf0e900fb272089c9fd299ea14bfccb1d1c2c0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0xa5e9c917b4b821e4e0a5bbefce078ab6540d6b5e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0x0da7096f14303eddd634c0241963c064e0244984 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/soneium/0x96aa22baedc5a605357e0b9ae20ab6b10a472e03 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0x610e319b3a3ab56a0ed5562927d37c233774ba39 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0xdce053d9ba0fa2c5f772416b64f191158cbcc32e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0x5f835420502a7702de50cd0e78d8aa3608b2137e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0x3672722f9c4413c63d8e275bea51ee526449a92d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0x0d8322f0422664ccf845fccedc10a82b292f9d3a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0x494d68e3cab640fa50f4c1b3e2499698d1a173a0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0x02371da6173cf95623da4189e68912233cc7107c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0x7c3c2a92598d6d77e57fd55c50e99af4b291f595 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0x6787de741bb42ca7ff7dd1b9aad6098c850cdc6a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0x56505f73cf8f5ef637bb37d9e635c3f520c9b0e5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0xad710fbc1161b26ea427c158f49a93f6d9d871b4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0x21b8065d10f73ee2e260e5b47d3344d3ced7596e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0xa43fe16908251ee70ef74718545e4fe6c5ccec9f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0x9c4fe5ffd9a9fc5678cfbd93aa2d4fd684b67c4c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0x0d4a11d5eeaac28ec3f61d100daf4d40471f1852 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0xbb2b8038a1640196fbe3e38816f3e67cba72d940 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0x7b73644935b8e68019ac6356c40661e1bc315860 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0xca7c2771d248dcbe09eabe0ce57a62e18da178c0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0xa478c2975ab1ea89e8196811f51a7b7ade33eb11 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0x52c77b0cb827afbad022e6d6caf2c44452edbc39 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0xc2eab7d33d3cb97692ecb231a5d0e4a649cb539d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0x180efc1349a69390ade25667487a826164c9c6e4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0x43de4318b6eb91a7cf37975dbb574396a7b5b5c6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0x9ec9367b8c4dd45ec8e7b800b1f719251053ad60 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0x25647e01bd0967c1b9599fa3521939871d1d0888 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0x3041cbd36888becc7bbcbc0045e3b1f144466f5f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0xe46935ae80e05cdebd4a4008b6ccaa36d2845370 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0x09d1d767edf8fa23a64c51fa559e0688e526812f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0xc555d55279023e732ccd32d812114caf5838fd46 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0x48d20b3e529fb3dd7d91293f80638df582ab2daa - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0x2a6c340bcbb0a79d3deecd3bc5cbc2605ea9259f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0xcaa004418eb42cdf00cb057b7c9e28f0ffd840a5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0x1ffec7119e315b15852557f654ae0052f76e6ae1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0x8c1c499b1796d7f3c2521ac37186b52de024e58c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0x4028daac072e492d34a3afdbef0ba7e35d8b55c4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0xd3d2e2692501a5c9ca623199d38826e513033a17 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0x6ada49aeccf6e556bb7a35ef0119cc8ca795294a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0x9ff68f61ca5eb0c6606dc517a9d44001e564bb66 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0xddd23787a6b80a794d952f5fb036d0b31a8e6aff - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0x470e8de2ebaef52014a47cb5e6af86884947f08c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0xda2d09fbbf8ee4b5051a0e9b562c5fcb4b393b18 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0xf6c4e4f339912541d3f8ed99dba64a1372af5e5b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0x7924a818013f39cf800f5589ff1f1f0def54f31f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0x69c7bd26512f52bf6f76fab834140d13dda673ca - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0x2caccf71bdf8fff97c06a46eca29b611b1a74b5e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0xaf996125e98b5804c00ffdb4f7ff386307c99a00 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0x0f23d49bc92ec52ff591d091b3e16c937034496e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0x0b07188b12e3bba6a680e553e23c4079e98a034b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0xb771f724c504b329623b0ce9199907137670600e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0xc7e6b676bfc73ae40bcc4577f22aab1682c691c6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0x6ec94f50cadcc79984463688de42a0ca696ec2db - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0xaf21b0ec0197e63a5c6cc30c8e947eb8165c6212 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0x5ced44f03ff443bbe14d8ea23bc24425fb89e3ed - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0xc5be99a02c6857f9eac67bbce58df5572498f40c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0xc91ef786fbf6d62858262c82c63de45085dea659 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0x97e1fcb93ae7267dbafad23f7b9afaa08264cfd8 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0xa1bf0e900fb272089c9fd299ea14bfccb1d1c2c0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0xa5e9c917b4b821e4e0a5bbefce078ab6540d6b5e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0x0da7096f14303eddd634c0241963c064e0244984 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/worldchain/0x96aa22baedc5a605357e0b9ae20ab6b10a472e03 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x3d7264539e6e3f596bb485e3091f3ae02ad01ef8 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0xeecb86c38c4667b46487255f41c6904df3d76f8f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0xa0769a3c6af68812bb3a5cbd511f7879033440eb - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x23c77a553aac0ad009441c856c05d117c1131e3d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0xedb4833e1cae54b3b7637f71edacf9abfcfbd1bd - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0xf8c42655373a280e8800beee44fcc12ffc99e797 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x4b2dfca17caadc23c9d28eb77ca27b52731e3aba - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x105a6f7c2f23270db6eed8d9ee8474323091d30c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x4152fdbf1ce1957b6fafd55737f96a26b787ee9a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x3f618967492945c02d5222d333e903345fde741a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0xff577f0e828a878743ecc5e2632cbf65cecf17cf - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x3e3dd517fec2e70eddba2a626422a4ba286e8c38 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x433587150898e706b21d68b48833cdf274987743 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0xc99bcff6564bafc70ba1b53c53a03541f780a546 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0xdc4359ca3a73c05b83759d3fe6618b499ff5f656 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0xa07028b453a1f6ac277e93f3a0ea73b4be5c7d63 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0xc1fcd2a14df1a10f91cdd0d9b6191ca264356eec - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x3f777c16829c7d1885a7a46912560f1ba764218d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0xcbd38eba8170f475063bcc2c56cb213f8db1f9e1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0xbeea3b382696669e0e67c08ea9f4aae8d528af0f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0xd864e059aae2d400ef3ab5b4d38b4370d63f1277 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x9e83bb5cf96fd382affb9f9f4d1bbdb49e7a5e7b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0xc4ea014402cddb4c6d2c4cb6d1c696eded93630a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0xd8ba14df11d963bd3c00ada3569a361d1810d4b1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x663a655203ec965a3f642772ef49ba1e99c1520d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x1017e7b5efbb2d230979cf166078c1a96cdeeaef - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0xbffd6eebdd42038067b10e04d3682e6373278ffe - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x7657d138111306459def4ba2285730f35ed6066f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x2dee3855d991a07ad3ed1fe3b26343320a122963 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0xe5bacf3e9b092c71de7cfe28124beb4c9d85783c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x80643ea8601be7f65362d4c2dc17b435dfa22762 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0xfc93ec2d9d0d390209365013aaa6358db9f77936 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x4d8295d7a043007760e1b2ea3ec07c93a906874d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x341584a7aa4cbfe1381009762aabdd0659542348 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0xed1def6744ba38d53ef80b59ac010b6d9392bcae - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0xffbd38130ff590d0c4d82e3851f6f4fdf9a3d3ab - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x1fa900dbb20ed45d18883849c00632bca16f6610 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x81111cd2ab53ccb3d060c0fd7b303654151c3b9d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x6dbdeea3d4127913420eedd6ff25c7c6765e104a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x42e2209e6d9b4ed0cce3d06a5a5d7b65d577cf5c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x21b8065d10f73ee2e260e5b47d3344d3ced7596e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0xa43fe16908251ee70ef74718545e4fe6c5ccec9f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x9c4fe5ffd9a9fc5678cfbd93aa2d4fd684b67c4c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x0d4a11d5eeaac28ec3f61d100daf4d40471f1852 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0xbb2b8038a1640196fbe3e38816f3e67cba72d940 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x7b73644935b8e68019ac6356c40661e1bc315860 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0xca7c2771d248dcbe09eabe0ce57a62e18da178c0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0xa478c2975ab1ea89e8196811f51a7b7ade33eb11 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x52c77b0cb827afbad022e6d6caf2c44452edbc39 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0xc2eab7d33d3cb97692ecb231a5d0e4a649cb539d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x180efc1349a69390ade25667487a826164c9c6e4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x43de4318b6eb91a7cf37975dbb574396a7b5b5c6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x9ec9367b8c4dd45ec8e7b800b1f719251053ad60 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x25647e01bd0967c1b9599fa3521939871d1d0888 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x3041cbd36888becc7bbcbc0045e3b1f144466f5f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0xe46935ae80e05cdebd4a4008b6ccaa36d2845370 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x09d1d767edf8fa23a64c51fa559e0688e526812f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0xc555d55279023e732ccd32d812114caf5838fd46 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x48d20b3e529fb3dd7d91293f80638df582ab2daa - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x2a6c340bcbb0a79d3deecd3bc5cbc2605ea9259f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0xcaa004418eb42cdf00cb057b7c9e28f0ffd840a5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x1ffec7119e315b15852557f654ae0052f76e6ae1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x8c1c499b1796d7f3c2521ac37186b52de024e58c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x4028daac072e492d34a3afdbef0ba7e35d8b55c4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0xd3d2e2692501a5c9ca623199d38826e513033a17 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x6ada49aeccf6e556bb7a35ef0119cc8ca795294a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x9ff68f61ca5eb0c6606dc517a9d44001e564bb66 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0xddd23787a6b80a794d952f5fb036d0b31a8e6aff - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x470e8de2ebaef52014a47cb5e6af86884947f08c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0xda2d09fbbf8ee4b5051a0e9b562c5fcb4b393b18 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0xf6c4e4f339912541d3f8ed99dba64a1372af5e5b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x7924a818013f39cf800f5589ff1f1f0def54f31f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x69c7bd26512f52bf6f76fab834140d13dda673ca - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x2caccf71bdf8fff97c06a46eca29b611b1a74b5e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0xaf996125e98b5804c00ffdb4f7ff386307c99a00 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x0f23d49bc92ec52ff591d091b3e16c937034496e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x0b07188b12e3bba6a680e553e23c4079e98a034b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0xb771f724c504b329623b0ce9199907137670600e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0xc7e6b676bfc73ae40bcc4577f22aab1682c691c6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x6ec94f50cadcc79984463688de42a0ca696ec2db - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0xaf21b0ec0197e63a5c6cc30c8e947eb8165c6212 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x5ced44f03ff443bbe14d8ea23bc24425fb89e3ed - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0xc5be99a02c6857f9eac67bbce58df5572498f40c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0xc91ef786fbf6d62858262c82c63de45085dea659 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x97e1fcb93ae7267dbafad23f7b9afaa08264cfd8 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0xa1bf0e900fb272089c9fd299ea14bfccb1d1c2c0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0xa5e9c917b4b821e4e0a5bbefce078ab6540d6b5e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x0da7096f14303eddd634c0241963c064e0244984 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zksync/0x96aa22baedc5a605357e0b9ae20ab6b10a472e03 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0x86c2fd1c99d8b7ff541767a4748b2eb38fd43da8 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0xbc59f8f3b275aa56a90d13bae7cce5e6e11a3b17 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0x1ed9b524d6f395ecc61aa24537f87a0482933069 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0xc3aee7c0e80f65ff13655955fa51d971e5d8d535 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0xaea25cc307dc4bac390816f3d85edcbc805c589d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0x33604ba99eb25c593cabd53c096c131a72a74752 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0x3c127629c99355f3671f128ebef2f49b4d17e4e1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0x4684c6198243fcd8bcfa706d8e29b6b0531c6172 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0xb14db2b0cdb55dcf97f7388a2f70b7ad28c80885 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0x21b8065d10f73ee2e260e5b47d3344d3ced7596e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0xa43fe16908251ee70ef74718545e4fe6c5ccec9f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0x9c4fe5ffd9a9fc5678cfbd93aa2d4fd684b67c4c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0x0d4a11d5eeaac28ec3f61d100daf4d40471f1852 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0xbb2b8038a1640196fbe3e38816f3e67cba72d940 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0x7b73644935b8e68019ac6356c40661e1bc315860 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0xca7c2771d248dcbe09eabe0ce57a62e18da178c0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0xa478c2975ab1ea89e8196811f51a7b7ade33eb11 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0x52c77b0cb827afbad022e6d6caf2c44452edbc39 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0xc2eab7d33d3cb97692ecb231a5d0e4a649cb539d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0x180efc1349a69390ade25667487a826164c9c6e4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0x43de4318b6eb91a7cf37975dbb574396a7b5b5c6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0x9ec9367b8c4dd45ec8e7b800b1f719251053ad60 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0x25647e01bd0967c1b9599fa3521939871d1d0888 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0x3041cbd36888becc7bbcbc0045e3b1f144466f5f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0xe46935ae80e05cdebd4a4008b6ccaa36d2845370 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0x09d1d767edf8fa23a64c51fa559e0688e526812f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0xc555d55279023e732ccd32d812114caf5838fd46 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0x48d20b3e529fb3dd7d91293f80638df582ab2daa - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0x2a6c340bcbb0a79d3deecd3bc5cbc2605ea9259f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0xcaa004418eb42cdf00cb057b7c9e28f0ffd840a5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0x1ffec7119e315b15852557f654ae0052f76e6ae1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0x8c1c499b1796d7f3c2521ac37186b52de024e58c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0x4028daac072e492d34a3afdbef0ba7e35d8b55c4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0xd3d2e2692501a5c9ca623199d38826e513033a17 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0x6ada49aeccf6e556bb7a35ef0119cc8ca795294a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0x9ff68f61ca5eb0c6606dc517a9d44001e564bb66 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0xddd23787a6b80a794d952f5fb036d0b31a8e6aff - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0x470e8de2ebaef52014a47cb5e6af86884947f08c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0xda2d09fbbf8ee4b5051a0e9b562c5fcb4b393b18 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0xf6c4e4f339912541d3f8ed99dba64a1372af5e5b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0x7924a818013f39cf800f5589ff1f1f0def54f31f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0x69c7bd26512f52bf6f76fab834140d13dda673ca - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0x2caccf71bdf8fff97c06a46eca29b611b1a74b5e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0xaf996125e98b5804c00ffdb4f7ff386307c99a00 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0x0f23d49bc92ec52ff591d091b3e16c937034496e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0x0b07188b12e3bba6a680e553e23c4079e98a034b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0xb771f724c504b329623b0ce9199907137670600e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0xc7e6b676bfc73ae40bcc4577f22aab1682c691c6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0x6ec94f50cadcc79984463688de42a0ca696ec2db - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0xaf21b0ec0197e63a5c6cc30c8e947eb8165c6212 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0x5ced44f03ff443bbe14d8ea23bc24425fb89e3ed - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0xc5be99a02c6857f9eac67bbce58df5572498f40c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0xc91ef786fbf6d62858262c82c63de45085dea659 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0x97e1fcb93ae7267dbafad23f7b9afaa08264cfd8 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0xa1bf0e900fb272089c9fd299ea14bfccb1d1c2c0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0xa5e9c917b4b821e4e0a5bbefce078ab6540d6b5e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0x0da7096f14303eddd634c0241963c064e0244984 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/zora/0x96aa22baedc5a605357e0b9ae20ab6b10a472e03 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x970a7749ecaa4394c8b2bf5f2471f41fd6b79288 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0xf7fd0860922bd3352e2dbaf725a182b74bf7a2e1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0xab22d1d671bb5cee8735c5ba29ea651ccda48a8e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0xb003df4b243f938132e8cadbeb237abc5a889fb4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x859f7092f56c43bb48bb46de7119d9c799716cdf - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0xe532b04d2f2e921dfec69e132e9214d2f82df304 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/ethereum/0x2a6c361b43a2edcae08e2bd5448e90e9369cced9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x00c8e9500f32237beecfc8179ae064606d457577 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x5151c83ad4e1c8c4ea0d1eaf91c246a8c6dab2a4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0xad397a0472503b066ab4b311d66fa1f659f4cb61 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x859f7092f56c43bb48bb46de7119d9c799716cdf - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0xe532b04d2f2e921dfec69e132e9214d2f82df304 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/arbitrum/0x2a6c361b43a2edcae08e2bd5448e90e9369cced9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0x859f7092f56c43bb48bb46de7119d9c799716cdf - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0xe532b04d2f2e921dfec69e132e9214d2f82df304 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/optimism/0x2a6c361b43a2edcae08e2bd5448e90e9369cced9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0xaf948ca24669f5ca9f5b6f90d9f2cef12f4a0d20 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0x7d4324293304797cb662c6ea1b904b6af2b485f5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/pools/polygon/0x859f7092f56c43bb48bb46de7119d9c799716cdf - 2025-10-17T22:04:33.647Z + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0xe532b04d2f2e921dfec69e132e9214d2f82df304 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/celo/0xb771f724c504b329623b0ce9199907137670600e + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0x2a6c361b43a2edcae08e2bd5448e90e9369cced9 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/celo/0xaf996125e98b5804c00ffdb4f7ff386307c99a00 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0x80e4f4f8bb89dab206c6bb4bddbf2ad72500394d - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/celo/0x69c7bd26512f52bf6f76fab834140d13dda673ca + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0xdfcfdf5dd0569d591e0bce28b5da3b13de09e3cb - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/celo/0x0b07188b12e3bba6a680e553e23c4079e98a034b + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0x73ccdeaa8957422c0a64e83f50c8814c7b33fe99 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/celo/0x5ced44f03ff443bbe14d8ea23bc24425fb89e3ed + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0x859f7092f56c43bb48bb46de7119d9c799716cdf - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/celo/0xa1bf0e900fb272089c9fd299ea14bfccb1d1c2c0 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0xe532b04d2f2e921dfec69e132e9214d2f82df304 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/celo/0xaf21b0ec0197e63a5c6cc30c8e947eb8165c6212 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/base/0x2a6c361b43a2edcae08e2bd5448e90e9369cced9 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/celo/0x0da7096f14303eddd634c0241963c064e0244984 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0xe3f5da07bcbfeb310ca65a6f98656dd41c3d3b4a - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/celo/0x2aeee741fa1e21120a21e57db9ee545428e683c9 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x329eeb1a58bdb3b804d1f94623c7a29a67a9b6b7 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/celo/0x96aa22baedc5a605357e0b9ae20ab6b10a472e03 + 2025-03-20T21:18:53.078Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x859f7092f56c43bb48bb46de7119d9c799716cdf - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0x65081cb48d74a32e9ccfed75164b8c09972dbcf1 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0xe532b04d2f2e921dfec69e132e9214d2f82df304 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0x5b16de420b1d093b962c0bc03dd91b6d423f8c4a + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x2a6c361b43a2edcae08e2bd5448e90e9369cced9 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0x18b268965e4e702bdf13469205937894b8ab0ee8 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/celo/0x0d76866f78fcbca2730e60c5997d59d6ba585613 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0x6b918c9f87b46a758c2b51bce427c8028dacb720 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/celo/0x859f7092f56c43bb48bb46de7119d9c799716cdf - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0xbda709a0665b340898856b8b29ff87079bb130d3 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/celo/0xe532b04d2f2e921dfec69e132e9214d2f82df304 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0xd49174dba635489c67fa628864c2d0d04824ebd8 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/celo/0x2a6c361b43a2edcae08e2bd5448e90e9369cced9 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0x9e2ef5522b2f9eac00912f25082f6e652123b54d + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0xd279f8bcc5f037f08cad776a0186acde0417c339 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0xc673d5164103357a7537c36438a6326776a14bbd + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0x1dfab2147401be78ea05f05a0379f86ec87a81cc - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0x8eb4b07affbd1083f42032eed35cd32e382ee8b7 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0x859f7092f56c43bb48bb46de7119d9c799716cdf - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0x12095933f1eeb066176dd2e41e5a2f8be6974616 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0xe532b04d2f2e921dfec69e132e9214d2f82df304 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0xfbc45ab96d02e150b2ddeb7dd4eacd3d8c674f4a + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0x2a6c361b43a2edcae08e2bd5448e90e9369cced9 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0xa9c6669de2c04c2adb22ac7a65d75b47fee30e35 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0x9c84f58bb51fabd18698efe95f5bab4f33e96e8f - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0x18ce92e7a37d994657f97c3defaf880a805f08d5 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0xf2b8c40f98dae03f261d41f312bd204f68430acc - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0x9d7151413833dcb13ae284d6a7fccd93989c47a1 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0xa8c90356d7c7dc508eef63670927bb15a0dc0298 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0xe97a0889d2b0660fddc144a8893b3ac9236756a6 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0x859f7092f56c43bb48bb46de7119d9c799716cdf - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0x75951bafe00c9ec72df8597444f4e8fa156110a7 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0xe532b04d2f2e921dfec69e132e9214d2f82df304 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0x8927058918e3cff6f55efe45a58db1be1f069e49 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0x2a6c361b43a2edcae08e2bd5448e90e9369cced9 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0x21b8065d10f73ee2e260e5b47d3344d3ced7596e + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0x9c84f58bb51fabd18698efe95f5bab4f33e96e8f - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0xa43fe16908251ee70ef74718545e4fe6c5ccec9f + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/blast/0x859f7092f56c43bb48bb46de7119d9c799716cdf - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/blast/0xe532b04d2f2e921dfec69e132e9214d2f82df304 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0x9c4fe5ffd9a9fc5678cfbd93aa2d4fd684b67c4c + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/blast/0x2a6c361b43a2edcae08e2bd5448e90e9369cced9 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0x0d4a11d5eeaac28ec3f61d100daf4d40471f1852 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/blast/0x9c84f58bb51fabd18698efe95f5bab4f33e96e8f - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0xbb2b8038a1640196fbe3e38816f3e67cba72d940 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/soneium/0x859f7092f56c43bb48bb46de7119d9c799716cdf - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0x7b73644935b8e68019ac6356c40661e1bc315860 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/soneium/0xe532b04d2f2e921dfec69e132e9214d2f82df304 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0xca7c2771d248dcbe09eabe0ce57a62e18da178c0 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/soneium/0x2a6c361b43a2edcae08e2bd5448e90e9369cced9 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0xa478c2975ab1ea89e8196811f51a7b7ade33eb11 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/soneium/0x9c84f58bb51fabd18698efe95f5bab4f33e96e8f - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0x52c77b0cb827afbad022e6d6caf2c44452edbc39 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/worldchain/0x48466012f56a07f89656dab41a996986602fd1aa - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0xc2eab7d33d3cb97692ecb231a5d0e4a649cb539d + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/worldchain/0x073b315457c5f4f5e658f6c06998a60abb5a7b90 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0x180efc1349a69390ade25667487a826164c9c6e4 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/worldchain/0xc19bc89ac024426f5a23c5bb8bc91d8017c90684 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0x43de4318b6eb91a7cf37975dbb574396a7b5b5c6 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/worldchain/0x859f7092f56c43bb48bb46de7119d9c799716cdf - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0x9ec9367b8c4dd45ec8e7b800b1f719251053ad60 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/worldchain/0xe532b04d2f2e921dfec69e132e9214d2f82df304 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0xe46935ae80e05cdebd4a4008b6ccaa36d2845370 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/worldchain/0x2a6c361b43a2edcae08e2bd5448e90e9369cced9 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0xc555d55279023e732ccd32d812114caf5838fd46 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/worldchain/0x9c84f58bb51fabd18698efe95f5bab4f33e96e8f - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0x3041cbd36888becc7bbcbc0045e3b1f144466f5f + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zksync/0x859f7092f56c43bb48bb46de7119d9c799716cdf - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0x25647e01bd0967c1b9599fa3521939871d1d0888 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zksync/0xe532b04d2f2e921dfec69e132e9214d2f82df304 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0x09d1d767edf8fa23a64c51fa559e0688e526812f + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zksync/0x2a6c361b43a2edcae08e2bd5448e90e9369cced9 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0x48d20b3e529fb3dd7d91293f80638df582ab2daa + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zksync/0x9c84f58bb51fabd18698efe95f5bab4f33e96e8f - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0x2a6c340bcbb0a79d3deecd3bc5cbc2605ea9259f + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zora/0x859f7092f56c43bb48bb46de7119d9c799716cdf - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0x1ffec7119e315b15852557f654ae0052f76e6ae1 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zora/0xe532b04d2f2e921dfec69e132e9214d2f82df304 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0xd3d2e2692501a5c9ca623199d38826e513033a17 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zora/0x2a6c361b43a2edcae08e2bd5448e90e9369cced9 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0xcaa004418eb42cdf00cb057b7c9e28f0ffd840a5 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zora/0x9c84f58bb51fabd18698efe95f5bab4f33e96e8f - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0x4028daac072e492d34a3afdbef0ba7e35d8b55c4 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0x4e6c10e2b505a1e8324aa64d3a92de764cf86783 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0x9ff68f61ca5eb0c6606dc517a9d44001e564bb66 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/base/0x953764548d1ca834e2b73fcd0d26a495336c99c8 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0x6ada49aeccf6e556bb7a35ef0119cc8ca795294a + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x4361aaffc616809a8536ea3d5afff3d1b87921a4 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0x8c1c499b1796d7f3c2521ac37186b52de024e58c + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0xc275a7390966e4bcbf331b837cd7316c4a3efa83 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0x7924a818013f39cf800f5589ff1f1f0def54f31f + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0x1e1dfff79d95725aaafd6b47af4fbc28d859ce28 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0xda2d09fbbf8ee4b5051a0e9b562c5fcb4b393b18 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0x7de4c593fe83417ca6ef98d7cf59c99d304f41c9 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0x470e8de2ebaef52014a47cb5e6af86884947f08c + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/base/0xd38d1ab8a150e6ee0ae70c86a8e9fb0c83255b76 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0xf6c4e4f339912541d3f8ed99dba64a1372af5e5b + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/base/0x06d7874037e622d6ef42294cf32eb259806cb1c6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0xb771f724c504b329623b0ce9199907137670600e + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/celo/0x37fd96e1d24f69f20172ab97040f806284a31ae5 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0x0f23d49bc92ec52ff591d091b3e16c937034496e + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/celo/0xa4bc5aa6229e6f2baa4b8851b19342a1d1217c08 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0xddd23787a6b80a794d952f5fb036d0b31a8e6aff + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0x1d6ae37db0e36305019fb3d4bad2750b8784adf9 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0x2caccf71bdf8fff97c06a46eca29b611b1a74b5e + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0x5c90b076af8cb73ef064efc09eae7936132bebcf - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0xaf996125e98b5804c00ffdb4f7ff386307c99a00 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0x9a601b332698e64aa90fd468ce858d504e43e7df - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0x69c7bd26512f52bf6f76fab834140d13dda673ca + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0xb431c70f800100d87554ac1142c4a94c5fe4c0c4 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0x0b07188b12e3bba6a680e553e23c4079e98a034b + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0x3885fbe4cd8aed7b7e9625923927fa1ce30662a3 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0xc5be99a02c6857f9eac67bbce58df5572498f40c + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0x3885fbe4cd8aed7b7e9625923927fa1ce30662a3 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0x5ced44f03ff443bbe14d8ea23bc24425fb89e3ed + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0x1bc877326b683d8aeaca8dbbc604b649e5ad78e6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0xc7e6b676bfc73ae40bcc4577f22aab1682c691c6 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0x3885fbe4cd8aed7b7e9625923927fa1ce30662a3 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0xa1bf0e900fb272089c9fd299ea14bfccb1d1c2c0 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0x3885fbe4cd8aed7b7e9625923927fa1ce30662a3 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0xaf21b0ec0197e63a5c6cc30c8e947eb8165c6212 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/base/0xedc625b74537ee3a10874f53d170e9c17a906b9c - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0xc91ef786fbf6d62858262c82c63de45085dea659 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/base/0x3885fbe4cd8aed7b7e9625923927fa1ce30662a3 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0x0da7096f14303eddd634c0241963c064e0244984 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x3885fbe4cd8aed7b7e9625923927fa1ce30662a3 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0x97e1fcb93ae7267dbafad23f7b9afaa08264cfd8 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/celo/0x3885fbe4cd8aed7b7e9625923927fa1ce30662a3 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0x2aeee741fa1e21120a21e57db9ee545428e683c9 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0x1b01fba73ff847e3d96162a8bcd5426f6cde56a6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0xa5e9c917b4b821e4e0a5bbefce078ab6540d6b5e + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0x5c75bfb6194d7d763d33ea292cbc50cda806451b - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/unichain/0x96aa22baedc5a605357e0b9ae20ab6b10a472e03 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0x67324985b5014b36b960273353deb3d96f2f18c2 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0xfae3f424a0a47706811521e3ee268f00cfb5c45e + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0x3885fbe4cd8aed7b7e9625923927fa1ce30662a3 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x7b602f98d71715916e7c963f51bfebc754ade2d0 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0x5281e311734869c64ca60ef047fd87759397efe6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0xb978a8c502ce97b04043036a91548b846067f9ea + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0xd3249cfbb4c00dd81f377ef5113848c5cc848780 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x804226ca4edb38e7ef56d16d16e92dc3223347a0 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0x83c6bdb0355c6b69277a388416e4dca992a81d6f - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x9ba9c677d19347abfba1d6b6d6ceb61942071561 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0xc566b786309bf2fe34dd48cea1267b13cead02bb - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x5e8754aaa7c2da42c218f40435cbdedbae88bfc5 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0x67324985b5014b36b960273353deb3d96f2f18c2 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0xeb7e0191f4054868d97f33ca7a4176b226ccbd2f + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0x3885fbe4cd8aed7b7e9625923927fa1ce30662a3 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0xd1356d360f37932059e5b89b7992692aa234eda6 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0x5281e311734869c64ca60ef047fd87759397efe6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x27b571f3e7f7827b13d927d2d59244e3e58a7d1a + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/blast/0x67324985b5014b36b960273353deb3d96f2f18c2 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0xe54dadf5b4f8779256e1bbb94eca00b124311208 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/blast/0x3885fbe4cd8aed7b7e9625923927fa1ce30662a3 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0xb2dc1235bf4b36628a8665aeb668bf202759528a + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/blast/0x5281e311734869c64ca60ef047fd87759397efe6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x724f6a02ed2eb82d8d45034b280903cf663731ab + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/soneium/0x67324985b5014b36b960273353deb3d96f2f18c2 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x2e587b9e7aa638d7eb7db5fe7447513bc4d0d28b + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/soneium/0x3885fbe4cd8aed7b7e9625923927fa1ce30662a3 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0xa7141c79d3d4a9ad67ba95d2b97fe7eed9fb92b3 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/soneium/0x5281e311734869c64ca60ef047fd87759397efe6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x01c7c6066ec10b1cd4821e13b9fb063680ffa083 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/worldchain/0x67324985b5014b36b960273353deb3d96f2f18c2 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x01ed0722f62a5dd3212b04aed2a2065623a705bb + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/worldchain/0x3885fbe4cd8aed7b7e9625923927fa1ce30662a3 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x28f577ed1cb3b0add148d745d9c0a2c0c40cc48d + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/worldchain/0x5281e311734869c64ca60ef047fd87759397efe6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x12089f58e0e7eadfc7d60cf1b1ed6839a811672a + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zksync/0x67324985b5014b36b960273353deb3d96f2f18c2 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x3859de09906bed098879cbef34d80817357244c7 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zksync/0x3885fbe4cd8aed7b7e9625923927fa1ce30662a3 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x178b99eaa2f6bc97d16f84edbce2e23ae6b8140b + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zksync/0x5281e311734869c64ca60ef047fd87759397efe6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x0e663593657b064e1bae76d28625df5d0ebd4421 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zora/0x67324985b5014b36b960273353deb3d96f2f18c2 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0xd18384f4398bf5415902ad5d87eaa96549fd4f1e + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zora/0x3885fbe4cd8aed7b7e9625923927fa1ce30662a3 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x97bb1788dcd64e17166931b87789cae97c154009 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zora/0x5281e311734869c64ca60ef047fd87759397efe6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x83b9a48793f4e5be12cfe1de3b1d55a83c4f1a9a + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0x4905220ca9d1001daa7be72e877243f4996002e0 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x6d61ae1b0d94941dc702581daaaafc7665d1c6d7 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/base/0x2fa9d6085c91151200e61a3e627d35001772c0d1 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x9a68f0bbced6d6f1f63f0a61215742377ac9d325 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0xa1d79325ec44d5d5a69119010823da0ec746e615 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0xcea3c61705b68fefd8da1048ca69db1344180fcb + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0xcbe856765eeec3fdc505ddebf9dc612da995e593 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0xf39f633a18042105454ff64c6abd07beb8cbcead + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0xcbe856765eeec3fdc505ddebf9dc612da995e593 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x7fd0055020a6d9c43c31c8ea755c5038ebd39722 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/blast/0xcbe856765eeec3fdc505ddebf9dc612da995e593 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0xe1e870fdff3ad67f2879542d841d8ab3e1406f4c + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/soneium/0xcbe856765eeec3fdc505ddebf9dc612da995e593 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0xc0a57a72cb7da55da2e50f664b1641570941618f + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/worldchain/0xcbe856765eeec3fdc505ddebf9dc612da995e593 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x43fb9c3fd6715e872272b0caab968a97692726eb + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zksync/0xcbe856765eeec3fdc505ddebf9dc612da995e593 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0xfc1e7bc60adfc151565c033521ee6ccbde027f54 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zora/0xcbe856765eeec3fdc505ddebf9dc612da995e593 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0xd399718256c5206d9586e866169dbdf131193e02 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0x57a63adebf02680c996a89413c324901dc0df801 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x9d1bcb75a7bc5defe7daed505c462572b5e022f9 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0xe5028e8e8fb3488c2003c09fffc00876bc974b1a - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0xfd584fcc3a74429d85c9a2294eaf0f566ddfd593 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0x704ad8d95c12d7fea531738faa94402725acb035 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x90fbfd28f16fa163b914be6b9b3eeeb1c3e02fe5 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0xdfea50f83fd27967741f2220110449d8663a1b4f - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x118bc22a76a71ff3187577422af2d57210277bab + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0x704ad8d95c12d7fea531738faa94402725acb035 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x94a0d838379e9703b69a777191d8a8951ada2e8e + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/blast/0x704ad8d95c12d7fea531738faa94402725acb035 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0xd312a35320a5936409edfd5e2eb0d17bcf99910b + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/soneium/0x704ad8d95c12d7fea531738faa94402725acb035 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x78b4c0301b7e6e3d31f259b3f112ced639f030a3 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/worldchain/0x704ad8d95c12d7fea531738faa94402725acb035 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0xaf4ceda77ec614099ffe67b7f4fe4427d52e75cc + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zksync/0x704ad8d95c12d7fea531738faa94402725acb035 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x0e7a5a8177d5711dc90ba9a6ded590060f52dd29 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zora/0x704ad8d95c12d7fea531738faa94402725acb035 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x9a601b332698e64aa90fd468ce858d504e43e7df + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0x841820459769cd629b10a36fd12e603938cc2679 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x15309a0e8c954c46601d8028153125c3b92884b6 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0xfff8d5fff6ee3226fa2f5d7d5d8c3ff785be9c74 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0xd494b33229ee2c857a43b94ee7117be05e5fd88c + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0x38fd16cebb0ec8bbc3041a9c40b4394d2743e77a - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0xdbd8a9c9d060973557f3ac7c7f642a9523529d68 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0x6200b24e69eb9a12c06d09f21b0335a9b0eeb227 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0xcec63fa2b71744e1cdd48f71e34acedd46b496aa + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0xfff8d5fff6ee3226fa2f5d7d5d8c3ff785be9c74 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x0fbb556c73226b12cd7cfb2bed56e77fc177f0ec + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0xfff8d5fff6ee3226fa2f5d7d5d8c3ff785be9c74 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0xc566b786309bf2fe34dd48cea1267b13cead02bb + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0xfff8d5fff6ee3226fa2f5d7d5d8c3ff785be9c74 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x21b8065d10f73ee2e260e5b47d3344d3ced7596e + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/base/0xbc9df7f489b3d5d38da7c5a6f7d751bdaa88f254 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0xa43fe16908251ee70ef74718545e4fe6c5ccec9f + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/base/0xd19c0dbbc5ba2ec4faa0e3fff892f0e95f23d9e0 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/base/0xfff8d5fff6ee3226fa2f5d7d5d8c3ff785be9c74 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x9c4fe5ffd9a9fc5678cfbd93aa2d4fd684b67c4c + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x76a67a7fb64253fb4b0d80e1adbd71bd5865d68a - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x0d4a11d5eeaac28ec3f61d100daf4d40471f1852 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0xfff8d5fff6ee3226fa2f5d7d5d8c3ff785be9c74 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0xbb2b8038a1640196fbe3e38816f3e67cba72d940 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/celo/0xfff8d5fff6ee3226fa2f5d7d5d8c3ff785be9c74 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x7b73644935b8e68019ac6356c40661e1bc315860 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0xff3d4af6aed14f62c202d55871f75c37951409c3 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0xca7c2771d248dcbe09eabe0ce57a62e18da178c0 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0xc799b2440fd3b8618b87dfbf6b0dabf218e92274 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0xa478c2975ab1ea89e8196811f51a7b7ade33eb11 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0x1ae3579d37ede91662003a9e9eed3997f3339eff - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x52c77b0cb827afbad022e6d6caf2c44452edbc39 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0x59ca2b1d97c98d585402531a7056321a9935e052 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0xc2eab7d33d3cb97692ecb231a5d0e4a649cb539d + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0x8d58a3348f302f2daad2bab66a83849d900effd9 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x180efc1349a69390ade25667487a826164c9c6e4 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0x5b16de420b1d093b962c0bc03dd91b6d423f8c4a - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x43de4318b6eb91a7cf37975dbb574396a7b5b5c6 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0xbd50f13489f8901e692862f440a5062d5f1aa062 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x9ec9367b8c4dd45ec8e7b800b1f719251053ad60 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0x1db0bca1d1a09e5a86213d349fa4fe33f8fe7fe2 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0xe46935ae80e05cdebd4a4008b6ccaa36d2845370 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0x20a613705ed7ee62821ae0879bfc47a2450eb0dd - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0xc555d55279023e732ccd32d812114caf5838fd46 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0x52393e963583683a75cff607792410bd48aa02d0 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x3041cbd36888becc7bbcbc0045e3b1f144466f5f + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0xfa20fa20b90c4c2411532c565cad7c14f9bc4a56 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x25647e01bd0967c1b9599fa3521939871d1d0888 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0x4d661231d973ff9c7c1b0e34a28b94bb70cb3894 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x09d1d767edf8fa23a64c51fa559e0688e526812f + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0x1c276bd1a3bc05b034d50a7369b0bd5b39223a65 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x48d20b3e529fb3dd7d91293f80638df582ab2daa + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0x338e7862fcae6c3dec1c49606c0fe297422fce39 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x2a6c340bcbb0a79d3deecd3bc5cbc2605ea9259f + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0xed79818e8168083883d5279c181eb76054d16c87 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x1ffec7119e315b15852557f654ae0052f76e6ae1 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0x308c6fbd6a14881af333649f17f2fde9cd75e2a6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0xd3d2e2692501a5c9ca623199d38826e513033a17 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0xf07a84f0732dfe8eea0d3961bcd8f62c761ff508 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0xcaa004418eb42cdf00cb057b7c9e28f0ffd840a5 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0xfff8d5fff6ee3226fa2f5d7d5d8c3ff785be9c74 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x4028daac072e492d34a3afdbef0ba7e35d8b55c4 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0xe945683b3462d2603a18bdfbb19261c6a4f03ad1 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x9ff68f61ca5eb0c6606dc517a9d44001e564bb66 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0xff878f095ea20f29d3b0967052c96526fc6ee14f - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x6ada49aeccf6e556bb7a35ef0119cc8ca795294a + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0x308c6fbd6a14881af333649f17f2fde9cd75e2a6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x8c1c499b1796d7f3c2521ac37186b52de024e58c + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0xf07a84f0732dfe8eea0d3961bcd8f62c761ff508 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x7924a818013f39cf800f5589ff1f1f0def54f31f + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0xfff8d5fff6ee3226fa2f5d7d5d8c3ff785be9c74 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0xda2d09fbbf8ee4b5051a0e9b562c5fcb4b393b18 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0xe945683b3462d2603a18bdfbb19261c6a4f03ad1 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x470e8de2ebaef52014a47cb5e6af86884947f08c + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/blast/0x308c6fbd6a14881af333649f17f2fde9cd75e2a6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0xf6c4e4f339912541d3f8ed99dba64a1372af5e5b + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/blast/0xf07a84f0732dfe8eea0d3961bcd8f62c761ff508 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0xb771f724c504b329623b0ce9199907137670600e + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/blast/0xfff8d5fff6ee3226fa2f5d7d5d8c3ff785be9c74 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x0f23d49bc92ec52ff591d091b3e16c937034496e + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/blast/0xe945683b3462d2603a18bdfbb19261c6a4f03ad1 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0xddd23787a6b80a794d952f5fb036d0b31a8e6aff + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/soneium/0xcd4255ceae51803a9333aa1a559991e17b024efc - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x2caccf71bdf8fff97c06a46eca29b611b1a74b5e + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/soneium/0xd68e61880190ebf2bda21a66d3064625866ddabd - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0xaf996125e98b5804c00ffdb4f7ff386307c99a00 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/soneium/0xecbe401654382b37ba3b5fea8950874b47e83a56 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x69c7bd26512f52bf6f76fab834140d13dda673ca + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/soneium/0x308c6fbd6a14881af333649f17f2fde9cd75e2a6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x0b07188b12e3bba6a680e553e23c4079e98a034b + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/soneium/0xf07a84f0732dfe8eea0d3961bcd8f62c761ff508 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0xc5be99a02c6857f9eac67bbce58df5572498f40c + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/soneium/0xfff8d5fff6ee3226fa2f5d7d5d8c3ff785be9c74 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x5ced44f03ff443bbe14d8ea23bc24425fb89e3ed + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/soneium/0xe945683b3462d2603a18bdfbb19261c6a4f03ad1 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0xc7e6b676bfc73ae40bcc4577f22aab1682c691c6 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/worldchain/0x1a4abfe77b9aec6259757805bd0d3c46fca6d494 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0xa1bf0e900fb272089c9fd299ea14bfccb1d1c2c0 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/worldchain/0x308c6fbd6a14881af333649f17f2fde9cd75e2a6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0xaf21b0ec0197e63a5c6cc30c8e947eb8165c6212 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/worldchain/0xf07a84f0732dfe8eea0d3961bcd8f62c761ff508 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0xc91ef786fbf6d62858262c82c63de45085dea659 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/worldchain/0xfff8d5fff6ee3226fa2f5d7d5d8c3ff785be9c74 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x0da7096f14303eddd634c0241963c064e0244984 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/worldchain/0xe945683b3462d2603a18bdfbb19261c6a4f03ad1 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x97e1fcb93ae7267dbafad23f7b9afaa08264cfd8 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zksync/0x308c6fbd6a14881af333649f17f2fde9cd75e2a6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x2aeee741fa1e21120a21e57db9ee545428e683c9 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zksync/0xf07a84f0732dfe8eea0d3961bcd8f62c761ff508 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0xa5e9c917b4b821e4e0a5bbefce078ab6540d6b5e + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zksync/0xfff8d5fff6ee3226fa2f5d7d5d8c3ff785be9c74 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/avalanche/0x96aa22baedc5a605357e0b9ae20ab6b10a472e03 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zksync/0xe945683b3462d2603a18bdfbb19261c6a4f03ad1 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0xf52b4b69123cbcf07798ae8265642793b2e8990c + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zora/0x308c6fbd6a14881af333649f17f2fde9cd75e2a6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0x85287626e78602d0da569332e419154b0bdea035 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zora/0xf07a84f0732dfe8eea0d3961bcd8f62c761ff508 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0x1cf4cfc7a984a474ab03f444ccedb30c3ae6f56c + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zora/0xfff8d5fff6ee3226fa2f5d7d5d8c3ff785be9c74 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0xf5a23bdd36a56ede75d503f6f643d5eaf25b1a8f + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zora/0xe945683b3462d2603a18bdfbb19261c6a4f03ad1 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0x57529f9e2a23cc53fc387b162d2ab0f1df3ed701 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0x7a11472726c3cf31b9a4c6900118aacd2a3e584a - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0xf4b646bc85458cc74497c773e2bc8b9ec1351e97 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0xfdfc89d953e044f84faa2ed4953190a066328ee0 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0xf08ff66b8ec0db053711f4989c40b084564f7de3 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0xff9722cb0712261a7f02a451dd178de10234ad0c - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0x0000fb2a9a0f3f35d72d7eedb8689c6db1d30225 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0x2aeee741fa1e21120a21e57db9ee545428e683c9 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0xbb60bb410182d8e96c41dfc92e017dd79f5100bf + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0x45e844b4d3b81aba43697ac73c55204bd7b727e7 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0x0f6c5fdcb927b99b3040c609bef07bdfc59a6173 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0xc0808a4d70b0c1d6811c3526ca358570516214fd - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0xaeba85e3328d6b77b58130f43815ac9c59603d38 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0x075b6f68b05fe21205f8827323a6f9b747b09e93 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0xbf796b95d09729815806dd50de07c1111aa3926f + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0x8ce4c6020fccf7428d0b6ec2d4410c0442626630 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0x63f59866a7e9a8628e7f1577ba55da134d64d8c2 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0x2aeee741fa1e21120a21e57db9ee545428e683c9 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0x78337095c61035056a85b7d430d6e9875f177ae2 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/blast/0x2aeee741fa1e21120a21e57db9ee545428e683c9 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0x258ba3b253e5cc3bab01c28d2f527aacd6d96793 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/soneium/0xa2e7d6051bed36e1552ba982dab7e6743785598e - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0x7b8086f5442f130a6be2d62dff02319018344feb + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/soneium/0x2aeee741fa1e21120a21e57db9ee545428e683c9 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0x9dfb11cf311a8fa1296f6958057f8bb02be51a4c + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/worldchain/0x2aeee741fa1e21120a21e57db9ee545428e683c9 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0xf95141f5552e592dc58c41e54d65d0f645ab7d7e + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zksync/0x2aeee741fa1e21120a21e57db9ee545428e683c9 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0x21b8065d10f73ee2e260e5b47d3344d3ced7596e + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zora/0x2aeee741fa1e21120a21e57db9ee545428e683c9 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0xa43fe16908251ee70ef74718545e4fe6c5ccec9f + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0xe6d7ebb9f1a9519dc06d557e03c522d53520e76a - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/base/0x529d2863a1521d0b57db028168fde2e97120017c - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0x9c4fe5ffd9a9fc5678cfbd93aa2d4fd684b67c4c + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/base/0x8d421b0d641193d67dd1aa024dab17fcde0bfc89 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0x0d4a11d5eeaac28ec3f61d100daf4d40471f1852 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x81c7294b66955824bc04acb642ae8dc58e6ce507 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0xbb2b8038a1640196fbe3e38816f3e67cba72d940 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0xafecdd2fc04f0939d7b6835529677608470c063d - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0x7b73644935b8e68019ac6356c40661e1bc315860 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/celo/0x1219b06380157f0ea0468f4f714d66e7f89d6956 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0xca7c2771d248dcbe09eabe0ce57a62e18da178c0 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0x8a82f2333101195b926e2c1dd56a116bb57d41c9 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0xa478c2975ab1ea89e8196811f51a7b7ade33eb11 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0x40857abd8b284205a81fe6e0dfcebe261f47d854 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0x52c77b0cb827afbad022e6d6caf2c44452edbc39 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/worldchain/0xe32915f74d76b6fe4945cb4c4c77474e2fdb6d63 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0xc2eab7d33d3cb97692ecb231a5d0e4a649cb539d + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0xe8a1f39c16eea2844e98f951d711bb4bb31557ad - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0x180efc1349a69390ade25667487a826164c9c6e4 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0x1688d62c82abebf4d33ecea96d983fc1627966f6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0x43de4318b6eb91a7cf37975dbb574396a7b5b5c6 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0x1688d62c82abebf4d33ecea96d983fc1627966f6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0x9ec9367b8c4dd45ec8e7b800b1f719251053ad60 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0x1688d62c82abebf4d33ecea96d983fc1627966f6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0xe46935ae80e05cdebd4a4008b6ccaa36d2845370 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0xfe530931da161232ec76a7c3bea7d36cf3811a0d - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0xc555d55279023e732ccd32d812114caf5838fd46 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0x1688d62c82abebf4d33ecea96d983fc1627966f6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0x3041cbd36888becc7bbcbc0045e3b1f144466f5f + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/base/0xc8da14d7467814a91384796db3ca2c273b30b361 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0x25647e01bd0967c1b9599fa3521939871d1d0888 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/base/0x0b1c2dcbbfa744ebd3fc17ff1a96a1e1eb4b2d69 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0x09d1d767edf8fa23a64c51fa559e0688e526812f + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/base/0x1688d62c82abebf4d33ecea96d983fc1627966f6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0x48d20b3e529fb3dd7d91293f80638df582ab2daa + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0xf150d29d92e7460a1531cbc9d1abeab33d6998e4 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0x2a6c340bcbb0a79d3deecd3bc5cbc2605ea9259f + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x7b39b6693f4ca739b01ca1c42a343c8159d96e23 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0x1ffec7119e315b15852557f654ae0052f76e6ae1 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x4c1d39e6b736a6a99105ee2a9e7c44bfc56af860 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0xd3d2e2692501a5c9ca623199d38826e513033a17 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x4f60db16e8235eae877305c1e9ef2f648be2971c - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0xcaa004418eb42cdf00cb057b7c9e28f0ffd840a5 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x1688d62c82abebf4d33ecea96d983fc1627966f6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0x4028daac072e492d34a3afdbef0ba7e35d8b55c4 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/celo/0x15689276dc6e7d2702f7e1900c42f885284ffc46 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0x9ff68f61ca5eb0c6606dc517a9d44001e564bb66 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/celo/0x1688d62c82abebf4d33ecea96d983fc1627966f6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0x6ada49aeccf6e556bb7a35ef0119cc8ca795294a + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0xcd695596b54099613b84862bc60907651c5917be - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0x8c1c499b1796d7f3c2521ac37186b52de024e58c + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0x1688d62c82abebf4d33ecea96d983fc1627966f6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0x7924a818013f39cf800f5589ff1f1f0def54f31f + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0xcbfbec4704a6e34a9c562796e0606750bcd73675 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0xda2d09fbbf8ee4b5051a0e9b562c5fcb4b393b18 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0x8bdacdf255a57b66d0adeb271f4f76403ff4dffa - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0x470e8de2ebaef52014a47cb5e6af86884947f08c + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0x1688d62c82abebf4d33ecea96d983fc1627966f6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0xf6c4e4f339912541d3f8ed99dba64a1372af5e5b + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/blast/0x1688d62c82abebf4d33ecea96d983fc1627966f6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0xb771f724c504b329623b0ce9199907137670600e + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/soneium/0x1688d62c82abebf4d33ecea96d983fc1627966f6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0x0f23d49bc92ec52ff591d091b3e16c937034496e + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/worldchain/0xb1baaf9b23dafb25775111a689c23f4016fd0a73 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0xddd23787a6b80a794d952f5fb036d0b31a8e6aff + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/worldchain/0x042c2083fb30d1324e102fffe527d1dc689d3c60 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0x2caccf71bdf8fff97c06a46eca29b611b1a74b5e + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/worldchain/0x1688d62c82abebf4d33ecea96d983fc1627966f6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0xaf996125e98b5804c00ffdb4f7ff386307c99a00 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zksync/0x1688d62c82abebf4d33ecea96d983fc1627966f6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0x69c7bd26512f52bf6f76fab834140d13dda673ca + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zora/0x1688d62c82abebf4d33ecea96d983fc1627966f6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0x0b07188b12e3bba6a680e553e23c4079e98a034b + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0x0b599ebf4e05af48b56d38e2dde520570c366460 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0xc5be99a02c6857f9eac67bbce58df5572498f40c + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0x7cbee2f89305beb746e2b0807365119a8ff2513b - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0x5ced44f03ff443bbe14d8ea23bc24425fb89e3ed + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0x399f600c9667ee748d31821b2df0c004b3432dc9 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0xc7e6b676bfc73ae40bcc4577f22aab1682c691c6 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0xe51a3d36ecad2d2fbeafb6295fe0beb4f8f8f30d - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0xa1bf0e900fb272089c9fd299ea14bfccb1d1c2c0 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0x23d17764f41aea93fdbb5beffa83571f0bf3f8b2 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0xaf21b0ec0197e63a5c6cc30c8e947eb8165c6212 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0x7cbee2f89305beb746e2b0807365119a8ff2513b - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0xc91ef786fbf6d62858262c82c63de45085dea659 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0x399f600c9667ee748d31821b2df0c004b3432dc9 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0x0da7096f14303eddd634c0241963c064e0244984 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0x7cbee2f89305beb746e2b0807365119a8ff2513b - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0x97e1fcb93ae7267dbafad23f7b9afaa08264cfd8 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0x399f600c9667ee748d31821b2df0c004b3432dc9 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0x2aeee741fa1e21120a21e57db9ee545428e683c9 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0xa236278bec0e0677a48527340cfb567b4e6e9adc - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0xa5e9c917b4b821e4e0a5bbefce078ab6540d6b5e + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0x7cbee2f89305beb746e2b0807365119a8ff2513b - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/blast/0x96aa22baedc5a605357e0b9ae20ab6b10a472e03 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0x399f600c9667ee748d31821b2df0c004b3432dc9 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0x21b8065d10f73ee2e260e5b47d3344d3ced7596e + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/base/0x7cbee2f89305beb746e2b0807365119a8ff2513b - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0xa43fe16908251ee70ef74718545e4fe6c5ccec9f + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/base/0x399f600c9667ee748d31821b2df0c004b3432dc9 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x387a86d863420ffa2ef88b2524e54513a0ded845 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0x9c4fe5ffd9a9fc5678cfbd93aa2d4fd684b67c4c + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x4b54900d3801b7a27657a0e63ce7a819365e0940 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0x0d4a11d5eeaac28ec3f61d100daf4d40471f1852 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0xc2b9d0b3db9afe1567d4952ec3aede504b7357cf - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0xbb2b8038a1640196fbe3e38816f3e67cba72d940 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x7cbee2f89305beb746e2b0807365119a8ff2513b - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0x7b73644935b8e68019ac6356c40661e1bc315860 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x399f600c9667ee748d31821b2df0c004b3432dc9 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0xca7c2771d248dcbe09eabe0ce57a62e18da178c0 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/celo/0x7cbee2f89305beb746e2b0807365119a8ff2513b - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0xa478c2975ab1ea89e8196811f51a7b7ade33eb11 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/celo/0x399f600c9667ee748d31821b2df0c004b3432dc9 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0x52c77b0cb827afbad022e6d6caf2c44452edbc39 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0x7cbee2f89305beb746e2b0807365119a8ff2513b - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0xc2eab7d33d3cb97692ecb231a5d0e4a649cb539d + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0x399f600c9667ee748d31821b2df0c004b3432dc9 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0x180efc1349a69390ade25667487a826164c9c6e4 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0x7cbee2f89305beb746e2b0807365119a8ff2513b - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0x43de4318b6eb91a7cf37975dbb574396a7b5b5c6 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0x399f600c9667ee748d31821b2df0c004b3432dc9 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0x9ec9367b8c4dd45ec8e7b800b1f719251053ad60 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/blast/0x7cbee2f89305beb746e2b0807365119a8ff2513b - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0xe46935ae80e05cdebd4a4008b6ccaa36d2845370 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/blast/0x399f600c9667ee748d31821b2df0c004b3432dc9 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0xc555d55279023e732ccd32d812114caf5838fd46 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/soneium/0x7cbee2f89305beb746e2b0807365119a8ff2513b - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0x3041cbd36888becc7bbcbc0045e3b1f144466f5f + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/soneium/0x399f600c9667ee748d31821b2df0c004b3432dc9 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0x25647e01bd0967c1b9599fa3521939871d1d0888 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/worldchain/0x7cbee2f89305beb746e2b0807365119a8ff2513b - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0x09d1d767edf8fa23a64c51fa559e0688e526812f + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/worldchain/0x399f600c9667ee748d31821b2df0c004b3432dc9 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0x48d20b3e529fb3dd7d91293f80638df582ab2daa + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zksync/0x7cbee2f89305beb746e2b0807365119a8ff2513b - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0x2a6c340bcbb0a79d3deecd3bc5cbc2605ea9259f + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zksync/0x399f600c9667ee748d31821b2df0c004b3432dc9 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0x1ffec7119e315b15852557f654ae0052f76e6ae1 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zora/0x7cbee2f89305beb746e2b0807365119a8ff2513b - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0xd3d2e2692501a5c9ca623199d38826e513033a17 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zora/0x399f600c9667ee748d31821b2df0c004b3432dc9 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0xcaa004418eb42cdf00cb057b7c9e28f0ffd840a5 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0x68fd2f81ac30ef3186dd618947f8be49edb27886 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0x4028daac072e492d34a3afdbef0ba7e35d8b55c4 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0xf9f588394ec5c3b05511368ce016de5fd3812446 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0x9ff68f61ca5eb0c6606dc517a9d44001e564bb66 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0x827f0a2a4376bc26729f398b865f424dc8456841 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0x6ada49aeccf6e556bb7a35ef0119cc8ca795294a + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0xd8cc6bfdee087148c220e9141a075d18418abbac - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0x8c1c499b1796d7f3c2521ac37186b52de024e58c + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x944b4364cd6faddfe8f83865aabdbf57d0d9358c - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0x7924a818013f39cf800f5589ff1f1f0def54f31f + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0xdb18729070d3abdc72f9cd57d3b949540cc4486a - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0xda2d09fbbf8ee4b5051a0e9b562c5fcb4b393b18 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/celo/0xf55791afbb35ad42984f18d6fe3e1ff73d81900c - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0x470e8de2ebaef52014a47cb5e6af86884947f08c + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/celo/0x61ef8708fc240dc7f9f2c0d81c3124df2fd8829f - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0xf6c4e4f339912541d3f8ed99dba64a1372af5e5b + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/celo/0x87dec9a2589d9e6511df84c193561b3a16cf6238 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0xb771f724c504b329623b0ce9199907137670600e + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/celo/0x2ac5baa668a8a58fd0e302b9896717484fd217b0 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0x0f23d49bc92ec52ff591d091b3e16c937034496e + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/celo/0x6bab3afa6d0c42d539bcbc33ffb68c0406913413 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0xddd23787a6b80a794d952f5fb036d0b31a8e6aff + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/celo/0x14e577e42d45fd2200a9b0e31d87fe826467111a - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0x2caccf71bdf8fff97c06a46eca29b611b1a74b5e + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/celo/0x4495f525c4ecacf9713a51ec3e8d1e81d7dff870 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0xaf996125e98b5804c00ffdb4f7ff386307c99a00 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/celo/0xc5a5caebf3bf6220a3efa222710ab488943a73f8 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0x69c7bd26512f52bf6f76fab834140d13dda673ca + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/celo/0x40b3737b8984d14a2e8f96d8c680b2d475719fdf - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0x0b07188b12e3bba6a680e553e23c4079e98a034b + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0xe92c7cc875245a86faf088febe4614e1e318bef5 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0xc5be99a02c6857f9eac67bbce58df5572498f40c + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0xdb6e3b03a3255e5ddfc99b98f18e6e8557a2ed96 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0x5ced44f03ff443bbe14d8ea23bc24425fb89e3ed + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/base/0x9331f571f79d1e186a095c93756b5680a43932ac - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0xc7e6b676bfc73ae40bcc4577f22aab1682c691c6 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0xe1acb466421ed24dd8bd381d1205bad0ad43ca9c - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0xa1bf0e900fb272089c9fd299ea14bfccb1d1c2c0 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0x2876ce407e520ba49177d83ff4e2c7338b6eeae4 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0xaf21b0ec0197e63a5c6cc30c8e947eb8165c6212 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0xf12533a96712133d9bb97c24de5bcf52f48851bd - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0xc91ef786fbf6d62858262c82c63de45085dea659 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0x873056a02255872514f05249d93228d788fe4fb4 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0x0da7096f14303eddd634c0241963c064e0244984 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0x28dfa7adabff80fdf4ffd7db0c0796fb86d26700 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0x97e1fcb93ae7267dbafad23f7b9afaa08264cfd8 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0x1b4a3f6ad0b7254e373f47a3e442eea60af693fc - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0x2aeee741fa1e21120a21e57db9ee545428e683c9 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0x929fcf81102c5577243ee614c2c455acd6681f1a - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0xa5e9c917b4b821e4e0a5bbefce078ab6540d6b5e + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0x873056a02255872514f05249d93228d788fe4fb4 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/soneium/0x96aa22baedc5a605357e0b9ae20ab6b10a472e03 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0x3202c46666e774b44ba463eafaa6da9a968a058f - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0x610e319b3a3ab56a0ed5562927d37c233774ba39 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0x873056a02255872514f05249d93228d788fe4fb4 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0xdce053d9ba0fa2c5f772416b64f191158cbcc32e + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0xa6a2d598365cd758ba0a72cd95b7f8805248a5d5 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0x5f835420502a7702de50cd0e78d8aa3608b2137e + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0x873056a02255872514f05249d93228d788fe4fb4 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0x494d68e3cab640fa50f4c1b3e2499698d1a173a0 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/base/0x052e77018bfb98dc6373c37d757bb1d3fad09ec5 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0x02371da6173cf95623da4189e68912233cc7107c + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/base/0xeff7f8fe083d7a446717b992bf84391253e54789 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0x6787de741bb42ca7ff7dd1b9aad6098c850cdc6a + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/base/0xf9bb9137256193af73d2e8b3e377727756fd98be - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0x7c3c2a92598d6d77e57fd55c50e99af4b291f595 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/base/0x873056a02255872514f05249d93228d788fe4fb4 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0x56505f73cf8f5ef637bb37d9e635c3f520c9b0e5 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x7840fafdea1292068c1167f5da035a54bd31fae5 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0xad710fbc1161b26ea427c158f49a93f6d9d871b4 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0xed85676d391be656e6efa22ea4f3c663ffdd1683 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0x21b8065d10f73ee2e260e5b47d3344d3ced7596e + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0xe6b4903642b4a1637d7b411d009af5c91617860c - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0xa43fe16908251ee70ef74718545e4fe6c5ccec9f + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x6a61d4c9c359d2ce0203378983beb4f69f85106c - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x873056a02255872514f05249d93228d788fe4fb4 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0x9c4fe5ffd9a9fc5678cfbd93aa2d4fd684b67c4c + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/celo/0x873056a02255872514f05249d93228d788fe4fb4 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0x0d4a11d5eeaac28ec3f61d100daf4d40471f1852 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0x97e4442bca2c069f9060f3b8eef52eb25c98c245 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0xbb2b8038a1640196fbe3e38816f3e67cba72d940 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0xd40be16e9c2bddd7f36dd31c1ae39ca6cb2b20ce - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0x7b73644935b8e68019ac6356c40661e1bc315860 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0x873056a02255872514f05249d93228d788fe4fb4 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0xca7c2771d248dcbe09eabe0ce57a62e18da178c0 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0x343fd171caf4f0287ae6b87d75a8964dc44516ab - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0xa478c2975ab1ea89e8196811f51a7b7ade33eb11 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0x40f4928332584198d5c68f3f39631245dda5c200 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0x52c77b0cb827afbad022e6d6caf2c44452edbc39 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0xeb2a20f397c37f9f36799dab5dbe762da58c5194 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0xc2eab7d33d3cb97692ecb231a5d0e4a649cb539d + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0x873056a02255872514f05249d93228d788fe4fb4 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0x180efc1349a69390ade25667487a826164c9c6e4 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0x343fd171caf4f0287ae6b87d75a8964dc44516ab - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0x43de4318b6eb91a7cf37975dbb574396a7b5b5c6 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/blast/0x873056a02255872514f05249d93228d788fe4fb4 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0x9ec9367b8c4dd45ec8e7b800b1f719251053ad60 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/blast/0x343fd171caf4f0287ae6b87d75a8964dc44516ab - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0xe46935ae80e05cdebd4a4008b6ccaa36d2845370 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/soneium/0x873056a02255872514f05249d93228d788fe4fb4 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0xc555d55279023e732ccd32d812114caf5838fd46 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/soneium/0x343fd171caf4f0287ae6b87d75a8964dc44516ab - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0x3041cbd36888becc7bbcbc0045e3b1f144466f5f + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/worldchain/0x873056a02255872514f05249d93228d788fe4fb4 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0x25647e01bd0967c1b9599fa3521939871d1d0888 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/worldchain/0x343fd171caf4f0287ae6b87d75a8964dc44516ab - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0x09d1d767edf8fa23a64c51fa559e0688e526812f + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zksync/0x873056a02255872514f05249d93228d788fe4fb4 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0x48d20b3e529fb3dd7d91293f80638df582ab2daa + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zksync/0x343fd171caf4f0287ae6b87d75a8964dc44516ab - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0x2a6c340bcbb0a79d3deecd3bc5cbc2605ea9259f + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zora/0x873056a02255872514f05249d93228d788fe4fb4 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0x1ffec7119e315b15852557f654ae0052f76e6ae1 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zora/0x343fd171caf4f0287ae6b87d75a8964dc44516ab - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0xd3d2e2692501a5c9ca623199d38826e513033a17 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0x53ead11073fc0651dce70572666f0ed0752abfea - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0xcaa004418eb42cdf00cb057b7c9e28f0ffd840a5 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/base/0xa0ca5bebc42cdbf3623b1c09206ae4e3975b0fc7 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0x4028daac072e492d34a3afdbef0ba7e35d8b55c4 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0x18aa3faeedb077aa604d748587093adcc9c5172b - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0x9ff68f61ca5eb0c6606dc517a9d44001e564bb66 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/base/0xb655dc66ecead581d1f1a5759c2c37c2dbef2275 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0x6ada49aeccf6e556bb7a35ef0119cc8ca795294a + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/base/0x2e8b5cf35680f8b7df0957d05c6a0a4ae1d00cde - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0x8c1c499b1796d7f3c2521ac37186b52de024e58c + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/base/0x87c0676255be413399c9a205d9cbeb2a04814cca - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0x7924a818013f39cf800f5589ff1f1f0def54f31f + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0xc09b0b0bde3b1f97ed3d3b5f5b0a74a74ee99768 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0xda2d09fbbf8ee4b5051a0e9b562c5fcb4b393b18 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x9950c1f3754fb8a3ebbaf24b8573cafc7474c00f - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0x470e8de2ebaef52014a47cb5e6af86884947f08c + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x9fca4c864a30c03c21cc8743ee0c73312deda0ec - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0xf6c4e4f339912541d3f8ed99dba64a1372af5e5b + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/worldchain/0x9b03dd0d04c2953a6a74471c53f24f15f592df1e - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0xb771f724c504b329623b0ce9199907137670600e + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0x260e34c78b27ece3fc6de8cd170e3ffb49fac35f - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0x0f23d49bc92ec52ff591d091b3e16c937034496e + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0x29f07e631a2f990e1f6117c6285a44e746b1f090 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0xddd23787a6b80a794d952f5fb036d0b31a8e6aff + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0x3dc10d7bfb94eeb009203e84a653e5764f71771d - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0x2caccf71bdf8fff97c06a46eca29b611b1a74b5e + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0x29f07e631a2f990e1f6117c6285a44e746b1f090 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0xaf996125e98b5804c00ffdb4f7ff386307c99a00 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/base/0x29f07e631a2f990e1f6117c6285a44e746b1f090 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0x69c7bd26512f52bf6f76fab834140d13dda673ca + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x0e5828c7eb7c7c9066201718f7ff044cf9fa10d9 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0x0b07188b12e3bba6a680e553e23c4079e98a034b + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x29f07e631a2f990e1f6117c6285a44e746b1f090 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0xc5be99a02c6857f9eac67bbce58df5572498f40c + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/celo/0x29f07e631a2f990e1f6117c6285a44e746b1f090 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0x5ced44f03ff443bbe14d8ea23bc24425fb89e3ed + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0x29f07e631a2f990e1f6117c6285a44e746b1f090 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0xc7e6b676bfc73ae40bcc4577f22aab1682c691c6 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0xb0828467656ef9a9804e0d57da3fbd79374877f4 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0xa1bf0e900fb272089c9fd299ea14bfccb1d1c2c0 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0x29f07e631a2f990e1f6117c6285a44e746b1f090 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0xaf21b0ec0197e63a5c6cc30c8e947eb8165c6212 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/blast/0x29f07e631a2f990e1f6117c6285a44e746b1f090 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0xc91ef786fbf6d62858262c82c63de45085dea659 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/soneium/0x29f07e631a2f990e1f6117c6285a44e746b1f090 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0x0da7096f14303eddd634c0241963c064e0244984 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/worldchain/0x075138693e0d65fa038753e3f3f97aad0989dd43 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0x97e1fcb93ae7267dbafad23f7b9afaa08264cfd8 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/worldchain/0x29f07e631a2f990e1f6117c6285a44e746b1f090 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0x2aeee741fa1e21120a21e57db9ee545428e683c9 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zksync/0x29f07e631a2f990e1f6117c6285a44e746b1f090 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0xa5e9c917b4b821e4e0a5bbefce078ab6540d6b5e + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zora/0x29f07e631a2f990e1f6117c6285a44e746b1f090 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/worldchain/0x96aa22baedc5a605357e0b9ae20ab6b10a472e03 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0xcd83055557536eff25fd0eafbc56e74a1b4260b3 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x3d7264539e6e3f596bb485e3091f3ae02ad01ef8 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0x3016a43b482d0480460f6625115bd372fe90c6bf - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0xeecb86c38c4667b46487255f41c6904df3d76f8f + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0x29f07e631a2f990e1f6117c6285a44e746b1f090 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x23c77a553aac0ad009441c856c05d117c1131e3d + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0x91308bc9ce8ca2db82aa30c65619856cc939d907 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0xa0769a3c6af68812bb3a5cbd511f7879033440eb + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0x1442097733acf0a2b5c4ab422f1c0186e95d52ba - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0xf8c42655373a280e8800beee44fcc12ffc99e797 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0x3016a43b482d0480460f6625115bd372fe90c6bf - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x4b2dfca17caadc23c9d28eb77ca27b52731e3aba + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0x29f07e631a2f990e1f6117c6285a44e746b1f090 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x105a6f7c2f23270db6eed8d9ee8474323091d30c + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0x3016a43b482d0480460f6625115bd372fe90c6bf - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x4152fdbf1ce1957b6fafd55737f96a26b787ee9a + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0x3016a43b482d0480460f6625115bd372fe90c6bf - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x3f618967492945c02d5222d333e903345fde741a + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/base/0x393e58375ca7bcaa89ed90e661ee7cc46466eccf - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0xff577f0e828a878743ecc5e2632cbf65cecf17cf + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/base/0x3016a43b482d0480460f6625115bd372fe90c6bf - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x3e3dd517fec2e70eddba2a626422a4ba286e8c38 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x3016a43b482d0480460f6625115bd372fe90c6bf - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x433587150898e706b21d68b48833cdf274987743 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/celo/0x3016a43b482d0480460f6625115bd372fe90c6bf - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0xc99bcff6564bafc70ba1b53c53a03541f780a546 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0x91020dd18c800eb5fee593ad58902b0ebdcfbfd2 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0xdc4359ca3a73c05b83759d3fe6618b499ff5f656 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0x3016a43b482d0480460f6625115bd372fe90c6bf - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0xa07028b453a1f6ac277e93f3a0ea73b4be5c7d63 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0x3016a43b482d0480460f6625115bd372fe90c6bf - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0xc1fcd2a14df1a10f91cdd0d9b6191ca264356eec + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/blast/0xa4f1768e3e1cd62c6faf4deab1ccef804a50c34e - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x3f777c16829c7d1885a7a46912560f1ba764218d + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/blast/0x3016a43b482d0480460f6625115bd372fe90c6bf - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0xcbd38eba8170f475063bcc2c56cb213f8db1f9e1 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/soneium/0x3016a43b482d0480460f6625115bd372fe90c6bf - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0xbeea3b382696669e0e67c08ea9f4aae8d528af0f + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/worldchain/0x3016a43b482d0480460f6625115bd372fe90c6bf - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0xd864e059aae2d400ef3ab5b4d38b4370d63f1277 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zksync/0x3016a43b482d0480460f6625115bd372fe90c6bf - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x9e83bb5cf96fd382affb9f9f4d1bbdb49e7a5e7b + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zora/0x3016a43b482d0480460f6625115bd372fe90c6bf - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0xc4ea014402cddb4c6d2c4cb6d1c696eded93630a + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0x4efc6d91b5170b670a1bd5cfb8d4ae50283400eb - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0xd8ba14df11d963bd3c00ada3569a361d1810d4b1 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0x993defde3e6ef50610eb6d994823dc82565ad3ba - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x663a655203ec965a3f642772ef49ba1e99c1520d + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/base/0xab04a352d4ec8d0f1812a4e5ebe7807fa67acd48 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x1017e7b5efbb2d230979cf166078c1a96cdeeaef + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0x49768b215014fac2c66680b03045fe32936b21e6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0xedb4833e1cae54b3b7637f71edacf9abfcfbd1bd + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0x5ae13baaef0620fdae1d355495dc51a17adb4082 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0xbffd6eebdd42038067b10e04d3682e6373278ffe + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/base/0xdea629c5587037d0925ff85f1961d95db62bedd6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x7657d138111306459def4ba2285730f35ed6066f + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0xba51d338a319f5e1b5da46065946576149874acf - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x2dee3855d991a07ad3ed1fe3b26343320a122963 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0xe47727f8c9256ab31dd97d431dfe9a5d9c098238 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0xe5bacf3e9b092c71de7cfe28124beb4c9d85783c + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0xafe235b62d5d7fe6335428d52df1a6204de002b8 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x80643ea8601be7f65362d4c2dc17b435dfa22762 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/soneium/0x76c54e356ccf357882e5632127983d6c975d7573 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0xfc93ec2d9d0d390209365013aaa6358db9f77936 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0xd491076c7316bc28fd4d35e3da9ab5286d079250 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x4d8295d7a043007760e1b2ea3ec07c93a906874d + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x7d98804119551f915a75ce278526ac7ee5077087 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0xed1def6744ba38d53ef80b59ac010b6d9392bcae + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0xcc630802c847766bf386fb7f4403ee5990f13e0a - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x341584a7aa4cbfe1381009762aabdd0659542348 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x9d66f536b5d0d4a6086ffbef06a12c5caa9a1460 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0xffbd38130ff590d0c4d82e3851f6f4fdf9a3d3ab + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0xf5ffdca301d3b2a0f4b3de313e4a176515d9bdbf - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x1fa900dbb20ed45d18883849c00632bca16f6610 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x6b86794e11cfb1d454d9b38ac5ab5ee2d1f87434 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x81111cd2ab53ccb3d060c0fd7b303654151c3b9d + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x89692e5e664c923b1dbbba13c57e07a0bacc5207 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x6dbdeea3d4127913420eedd6ff25c7c6765e104a + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x0ab8855c73d567f0ec633cddec43580c5f7e2555 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x42e2209e6d9b4ed0cce3d06a5a5d7b65d577cf5c + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0xe04b2344211942751334e848f717a27a10eb8c45 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x21b8065d10f73ee2e260e5b47d3344d3ced7596e + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/blast/0x75a0eae18bac14a3e9bebb4e2e5a12a926f2d230 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0xa43fe16908251ee70ef74718545e4fe6c5ccec9f + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/soneium/0x64850db70da55c031c9b6b8518502988b5734633 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/soneium/0xaacb6baf9cf61be287b8744a0e0c1027709dc26f - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x9c4fe5ffd9a9fc5678cfbd93aa2d4fd684b67c4c + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/soneium/0xd828b499b3ed234af94c4d5521c1a82a787da86e - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x0d4a11d5eeaac28ec3f61d100daf4d40471f1852 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/soneium/0x1846464de29a2e0d7cfbd3f350d7cb4675236d48 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0xbb2b8038a1640196fbe3e38816f3e67cba72d940 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0x919b20ac45304aeb09c9df5c604b3cd9d99a51ca - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x7b73644935b8e68019ac6356c40661e1bc315860 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0x5b2c9ef1bf180a844389c0fd42b15c8281e69052 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0xca7c2771d248dcbe09eabe0ce57a62e18da178c0 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0xe101e63ee82f4c8acc5e8f0e03da8b444be71c68 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0xa478c2975ab1ea89e8196811f51a7b7ade33eb11 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0x5b2c9ef1bf180a844389c0fd42b15c8281e69052 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x52c77b0cb827afbad022e6d6caf2c44452edbc39 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0xe101e63ee82f4c8acc5e8f0e03da8b444be71c68 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0xc2eab7d33d3cb97692ecb231a5d0e4a649cb539d + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0x5b2c9ef1bf180a844389c0fd42b15c8281e69052 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x180efc1349a69390ade25667487a826164c9c6e4 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0xe101e63ee82f4c8acc5e8f0e03da8b444be71c68 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x43de4318b6eb91a7cf37975dbb574396a7b5b5c6 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0x5b2c9ef1bf180a844389c0fd42b15c8281e69052 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x9ec9367b8c4dd45ec8e7b800b1f719251053ad60 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0xe101e63ee82f4c8acc5e8f0e03da8b444be71c68 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0xe46935ae80e05cdebd4a4008b6ccaa36d2845370 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/base/0x9144a6e8ad6f56db96dac444eb173831660ba3e5 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0xc555d55279023e732ccd32d812114caf5838fd46 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/base/0xfb559d225343a61884d46eee91c1a805759f758b - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x3041cbd36888becc7bbcbc0045e3b1f144466f5f + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/base/0x5b2c9ef1bf180a844389c0fd42b15c8281e69052 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x25647e01bd0967c1b9599fa3521939871d1d0888 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/base/0xe101e63ee82f4c8acc5e8f0e03da8b444be71c68 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x09d1d767edf8fa23a64c51fa559e0688e526812f + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x30db6dfdb8817765797bd62316e41f5f4e431e93 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x48d20b3e529fb3dd7d91293f80638df582ab2daa + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x7deef378b6befa291e2e255294e532b2c1bca419 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x2a6c340bcbb0a79d3deecd3bc5cbc2605ea9259f + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x8bfb0fb037b30562fdb7be3f71440575664ab74e - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x1ffec7119e315b15852557f654ae0052f76e6ae1 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x9338f9d099be4011991f8ca784d67acdf901f376 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0xd3d2e2692501a5c9ca623199d38826e513033a17 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x2e066d83ad074b1a43cd14af5e08f2cf8a275021 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0xcaa004418eb42cdf00cb057b7c9e28f0ffd840a5 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0xef947aa8af8160cf78455292eaf3deb6b39e4bef - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x4028daac072e492d34a3afdbef0ba7e35d8b55c4 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x5b2c9ef1bf180a844389c0fd42b15c8281e69052 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x9ff68f61ca5eb0c6606dc517a9d44001e564bb66 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0xe101e63ee82f4c8acc5e8f0e03da8b444be71c68 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x6ada49aeccf6e556bb7a35ef0119cc8ca795294a + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/celo/0x5b2c9ef1bf180a844389c0fd42b15c8281e69052 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x8c1c499b1796d7f3c2521ac37186b52de024e58c + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/celo/0xe101e63ee82f4c8acc5e8f0e03da8b444be71c68 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x7924a818013f39cf800f5589ff1f1f0def54f31f + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0x5b2c9ef1bf180a844389c0fd42b15c8281e69052 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0xda2d09fbbf8ee4b5051a0e9b562c5fcb4b393b18 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0xe101e63ee82f4c8acc5e8f0e03da8b444be71c68 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x470e8de2ebaef52014a47cb5e6af86884947f08c + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0x5b2c9ef1bf180a844389c0fd42b15c8281e69052 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0xf6c4e4f339912541d3f8ed99dba64a1372af5e5b + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0xe101e63ee82f4c8acc5e8f0e03da8b444be71c68 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0xb771f724c504b329623b0ce9199907137670600e + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/blast/0x5b2c9ef1bf180a844389c0fd42b15c8281e69052 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x0f23d49bc92ec52ff591d091b3e16c937034496e + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/blast/0xe101e63ee82f4c8acc5e8f0e03da8b444be71c68 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0xddd23787a6b80a794d952f5fb036d0b31a8e6aff + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/soneium/0x5b2c9ef1bf180a844389c0fd42b15c8281e69052 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x2caccf71bdf8fff97c06a46eca29b611b1a74b5e + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/soneium/0xe101e63ee82f4c8acc5e8f0e03da8b444be71c68 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0xaf996125e98b5804c00ffdb4f7ff386307c99a00 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/worldchain/0x5b2c9ef1bf180a844389c0fd42b15c8281e69052 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x69c7bd26512f52bf6f76fab834140d13dda673ca + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/worldchain/0xe101e63ee82f4c8acc5e8f0e03da8b444be71c68 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x0b07188b12e3bba6a680e553e23c4079e98a034b + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zksync/0x5b2c9ef1bf180a844389c0fd42b15c8281e69052 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0xc5be99a02c6857f9eac67bbce58df5572498f40c + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zksync/0xe101e63ee82f4c8acc5e8f0e03da8b444be71c68 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x5ced44f03ff443bbe14d8ea23bc24425fb89e3ed + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zora/0x5b2c9ef1bf180a844389c0fd42b15c8281e69052 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0xc7e6b676bfc73ae40bcc4577f22aab1682c691c6 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zora/0xe101e63ee82f4c8acc5e8f0e03da8b444be71c68 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0xa1bf0e900fb272089c9fd299ea14bfccb1d1c2c0 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0xc74f05c1e7b86fa42ab07ab4a1361286b9a90087 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0xaf21b0ec0197e63a5c6cc30c8e947eb8165c6212 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0x5bcebcee72f13004f1d00d7da7bf22b082f93f70 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0xc91ef786fbf6d62858262c82c63de45085dea659 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0x31fa55e03bad93c7f8affdd2ec616ebfde246001 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x0da7096f14303eddd634c0241963c064e0244984 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0x263f7b865de80355f91c00dfb975a821effbea24 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x97e1fcb93ae7267dbafad23f7b9afaa08264cfd8 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0xbc5592c48bf9d4354de4953f57de4f6295dd51b0 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x2aeee741fa1e21120a21e57db9ee545428e683c9 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0x5bcebcee72f13004f1d00d7da7bf22b082f93f70 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0xa5e9c917b4b821e4e0a5bbefce078ab6540d6b5e + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0x0893e340ee2b0263ddad2f3b8bd23dba11859aea - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zksync/0x96aa22baedc5a605357e0b9ae20ab6b10a472e03 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0x5bcebcee72f13004f1d00d7da7bf22b082f93f70 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0x86c2fd1c99d8b7ff541767a4748b2eb38fd43da8 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0x5bcebcee72f13004f1d00d7da7bf22b082f93f70 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0xbc59f8f3b275aa56a90d13bae7cce5e6e11a3b17 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/base/0x5bcebcee72f13004f1d00d7da7bf22b082f93f70 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0x1ed9b524d6f395ecc61aa24537f87a0482933069 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0xff8dd24ffd38eca6bbe58c7aca49abc2d9abb574 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0xc3aee7c0e80f65ff13655955fa51d971e5d8d535 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0xeab00687c6558cd648ec288f58de4b0a6de026ba - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0xaea25cc307dc4bac390816f3d85edcbc805c589d + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x4ef938b633d704f29e593a8b51148d43429d0bc4 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0x33604ba99eb25c593cabd53c096c131a72a74752 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x85144212383a34479f3b0abe6f6f9ee69240fb55 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0x3c127629c99355f3671f128ebef2f49b4d17e4e1 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x5bcebcee72f13004f1d00d7da7bf22b082f93f70 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0x4684c6198243fcd8bcfa706d8e29b6b0531c6172 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/celo/0x5bcebcee72f13004f1d00d7da7bf22b082f93f70 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0xb14db2b0cdb55dcf97f7388a2f70b7ad28c80885 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0x5bcebcee72f13004f1d00d7da7bf22b082f93f70 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0x21b8065d10f73ee2e260e5b47d3344d3ced7596e + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0x5bcebcee72f13004f1d00d7da7bf22b082f93f70 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0xa43fe16908251ee70ef74718545e4fe6c5ccec9f + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/blast/0x5bcebcee72f13004f1d00d7da7bf22b082f93f70 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/soneium/0x5bcebcee72f13004f1d00d7da7bf22b082f93f70 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0x9c4fe5ffd9a9fc5678cfbd93aa2d4fd684b67c4c + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/worldchain/0x5bcebcee72f13004f1d00d7da7bf22b082f93f70 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0x0d4a11d5eeaac28ec3f61d100daf4d40471f1852 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zksync/0x5bcebcee72f13004f1d00d7da7bf22b082f93f70 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0xbb2b8038a1640196fbe3e38816f3e67cba72d940 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zora/0x5bcebcee72f13004f1d00d7da7bf22b082f93f70 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0x7b73644935b8e68019ac6356c40661e1bc315860 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0x2837809fd68e4a4104af76bbec5b622b6146b2cb - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0xca7c2771d248dcbe09eabe0ce57a62e18da178c0 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0xa2107fa5b38d9bbd2c461d6edf11b11a50f6b974 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0xa478c2975ab1ea89e8196811f51a7b7ade33eb11 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/ethereum/0xc2b7888a8d7b62e2a518bbc79fbbd6b75da524b6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0x52c77b0cb827afbad022e6d6caf2c44452edbc39 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0xb4e27c8e10856daa165a852f44462d1ca945e25c - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0xc2eab7d33d3cb97692ecb231a5d0e4a649cb539d + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0x68ebadf62ee5acce5f8d64211d24b4710eeb2029 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0x180efc1349a69390ade25667487a826164c9c6e4 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0xa2107fa5b38d9bbd2c461d6edf11b11a50f6b974 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0x43de4318b6eb91a7cf37975dbb574396a7b5b5c6 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/arbitrum/0xc2b7888a8d7b62e2a518bbc79fbbd6b75da524b6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0x9ec9367b8c4dd45ec8e7b800b1f719251053ad60 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0x1b19825a9e32b1039080acb1e1f9271314938b96 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0xe46935ae80e05cdebd4a4008b6ccaa36d2845370 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0xa2107fa5b38d9bbd2c461d6edf11b11a50f6b974 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0xc555d55279023e732ccd32d812114caf5838fd46 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/optimism/0xc2b7888a8d7b62e2a518bbc79fbbd6b75da524b6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0x3041cbd36888becc7bbcbc0045e3b1f144466f5f + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0x718a038c465efd88962fafe1008d233bf52bef44 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0x25647e01bd0967c1b9599fa3521939871d1d0888 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0x9913a93c082fdc69f9e7d146b0e4ce9070d5a104 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0x09d1d767edf8fa23a64c51fa559e0688e526812f + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0xa2107fa5b38d9bbd2c461d6edf11b11a50f6b974 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0x48d20b3e529fb3dd7d91293f80638df582ab2daa + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/polygon/0xc2b7888a8d7b62e2a518bbc79fbbd6b75da524b6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0x2a6c340bcbb0a79d3deecd3bc5cbc2605ea9259f + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/base/0xb4cb800910b228ed3d0834cf79d697127bbb00e5 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0x1ffec7119e315b15852557f654ae0052f76e6ae1 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/base/0xa2107fa5b38d9bbd2c461d6edf11b11a50f6b974 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0xd3d2e2692501a5c9ca623199d38826e513033a17 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/base/0xc2b7888a8d7b62e2a518bbc79fbbd6b75da524b6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0xcaa004418eb42cdf00cb057b7c9e28f0ffd840a5 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x4610464356ba6bba15eec558619d84b72fea260f - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0x4028daac072e492d34a3afdbef0ba7e35d8b55c4 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x65d5b99ba46b29005856ddb7d5c6675aab77b204 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0x9ff68f61ca5eb0c6606dc517a9d44001e564bb66 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0x5066103530d8de7e582ff2f623c59f4b2eca8bf6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0x6ada49aeccf6e556bb7a35ef0119cc8ca795294a + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0xf7932a224761fb8df2db546dd0e587e60439b4b4 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0x8c1c499b1796d7f3c2521ac37186b52de024e58c + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0xb1c5374164f17fc74f7401fb4ea88c2604ba75c4 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0x7924a818013f39cf800f5589ff1f1f0def54f31f + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0xa2107fa5b38d9bbd2c461d6edf11b11a50f6b974 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0xda2d09fbbf8ee4b5051a0e9b562c5fcb4b393b18 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/bnb/0xc2b7888a8d7b62e2a518bbc79fbbd6b75da524b6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0x470e8de2ebaef52014a47cb5e6af86884947f08c + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/celo/0x57332c214e647063bb4c5a73e5a8b7bba79be1e4 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0xf6c4e4f339912541d3f8ed99dba64a1372af5e5b + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/celo/0x8917ba2a352c6d3e1acd5ea0a0bf7f203046149e - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0xb771f724c504b329623b0ce9199907137670600e + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/celo/0xa2107fa5b38d9bbd2c461d6edf11b11a50f6b974 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0x0f23d49bc92ec52ff591d091b3e16c937034496e + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/celo/0xc2b7888a8d7b62e2a518bbc79fbbd6b75da524b6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0xddd23787a6b80a794d952f5fb036d0b31a8e6aff + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0xa2107fa5b38d9bbd2c461d6edf11b11a50f6b974 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0x2caccf71bdf8fff97c06a46eca29b611b1a74b5e + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/unichain/0xc2b7888a8d7b62e2a518bbc79fbbd6b75da524b6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0xaf996125e98b5804c00ffdb4f7ff386307c99a00 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0x7145084e49429057f28d4c5c955cf277a027ae93 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0x69c7bd26512f52bf6f76fab834140d13dda673ca + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0xa2107fa5b38d9bbd2c461d6edf11b11a50f6b974 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0x0b07188b12e3bba6a680e553e23c4079e98a034b + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/avalanche/0xc2b7888a8d7b62e2a518bbc79fbbd6b75da524b6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0xc5be99a02c6857f9eac67bbce58df5572498f40c + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/blast/0xa2107fa5b38d9bbd2c461d6edf11b11a50f6b974 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0x5ced44f03ff443bbe14d8ea23bc24425fb89e3ed + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/blast/0xc2b7888a8d7b62e2a518bbc79fbbd6b75da524b6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0xc7e6b676bfc73ae40bcc4577f22aab1682c691c6 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/soneium/0xa2107fa5b38d9bbd2c461d6edf11b11a50f6b974 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0xa1bf0e900fb272089c9fd299ea14bfccb1d1c2c0 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/soneium/0xc2b7888a8d7b62e2a518bbc79fbbd6b75da524b6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0xaf21b0ec0197e63a5c6cc30c8e947eb8165c6212 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/worldchain/0xa2107fa5b38d9bbd2c461d6edf11b11a50f6b974 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0xc91ef786fbf6d62858262c82c63de45085dea659 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/worldchain/0xc2b7888a8d7b62e2a518bbc79fbbd6b75da524b6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0x0da7096f14303eddd634c0241963c064e0244984 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zksync/0xa2107fa5b38d9bbd2c461d6edf11b11a50f6b974 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0x97e1fcb93ae7267dbafad23f7b9afaa08264cfd8 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zksync/0xc2b7888a8d7b62e2a518bbc79fbbd6b75da524b6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0x2aeee741fa1e21120a21e57db9ee545428e683c9 + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zora/0xa2107fa5b38d9bbd2c461d6edf11b11a50f6b974 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0xa5e9c917b4b821e4e0a5bbefce078ab6540d6b5e + 2025-03-20T21:32:51.328Z 0.8 - https://app.uniswap.org/explore/pools/zora/0xc2b7888a8d7b62e2a518bbc79fbbd6b75da524b6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/pools/zora/0x96aa22baedc5a605357e0b9ae20ab6b10a472e03 + 2025-03-20T21:32:51.328Z 0.8 diff --git a/apps/web/public/sitemap.xml b/apps/web/public/sitemap.xml index f344621757a..b035ec1e5db 100644 --- a/apps/web/public/sitemap.xml +++ b/apps/web/public/sitemap.xml @@ -9,7 +9,4 @@ https://app.uniswap.org/pools-sitemap.xml - - https://app.uniswap.org/nfts-sitemap.xml - diff --git a/apps/web/public/vercel-csp.json b/apps/web/public/staging-csp.json similarity index 69% rename from apps/web/public/vercel-csp.json rename to apps/web/public/staging-csp.json index bdf0d87f9a3..691682ae648 100644 --- a/apps/web/public/vercel-csp.json +++ b/apps/web/public/staging-csp.json @@ -1,4 +1,5 @@ { "defaultSrc": ["https://vercel.live/", "https://vercel.com"], - "scriptSrc": ["'sha256-jhb+qiJiVBxruYChJWIrsskRE2fAoV/F/B10cAoiSB0='"] + "scriptSrc": ["'sha256-jhb+qiJiVBxruYChJWIrsskRE2fAoV/F/B10cAoiSB0='"], + "connectSrc": ["https://*.corn-staging.com"] } diff --git a/apps/web/public/tokens-sitemap.xml b/apps/web/public/tokens-sitemap.xml index cc16efb4f28..a2201a012ea 100644 --- a/apps/web/public/tokens-sitemap.xml +++ b/apps/web/public/tokens-sitemap.xml @@ -2,11117 +2,3382 @@ https://app.uniswap.org/explore/tokens/ethereum/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xdac17f958d2ee523a2206206994597c13d831ec7 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x2260fac5e5542a773aa44fbcfedf7c193bc2c599 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x6982508145454ce325ddbe47a25d4ec3d2311933 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x6b175474e89094c44da98b954eedeac495271d0f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x6123b0049f904d730db3c36a31167d9d4121fa6b - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x1f9840a85d5af5bf1d1762f925bdaddc4201f984 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xcf0c122c6b73ff809c693db761e7baebe62b6a2e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xfaba6f8e4a5e8ab82f62fe7c39859fa577269be3 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x58cb30368ceb2d194740b144eab4c2da8a917dcb - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x4c9edd5852cd905f086c759e8383e09bff1e68b3 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xaaee1a9723aadb7afa2810263653a34ba2c21c7a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x514910771af9ca656af840dff83e8264ecf986ca - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x5b7533812759b45c2b44c19e320ba2cd2681b542 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xae78736cd615f374d3085123a210448e74fc6393 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xb9f599ce614feb2e1bbe58f180f370d05b39344e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xd5f7838f5c461feff7fe49ea5ebaf7728bb0adfa - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xd31a59c85ae9d8edefec411d448f90841571b89c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x6a7eff1e2c355ad6eb91bebb5ded49257f3fed98 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x576e2bed8f7b46d34016198911cdf9886f78bea7 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x1258d60b224c0c5cd888d37bbf31aa5fcfb7e870 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x62d0a8458ed7719fdaf978fe5929c6d342b0bfce - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x77e06c9eccf2e797fd462a92b6d7642ef85b0a44 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x24fcfc492c1393274b6bcd568ac9e225bec93584 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x27702a26126e0b3702af63ee09ac4d1a084ef628 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xd46ba6d942050d489dbd938a2c909a5d5039a161 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xbe9895146f7af43049ca1c1ae358b0541ea49704 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x72f713d11480dcf08b37e1898670e736688d218d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x0001a500a6b18995b03f44bb040a5ffc28e45cb0 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x9e9fbde7c7a83c43913bddc8779158f1368f0413 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x5f98805a4e8be255a32880fdec7f6728c6568ba0 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x2b591e99afe9f32eaa6214f7b7629768c40eeb39 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x1ae7e1d0ce06364ced9ad58225a1705b3e5db92b - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x046eee2cc3188071c02bfc1745a6b17c656e3f3d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x84018071282d4b2996272659d9c01cb08dd7327f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x12970e6868f88f6557b76120662c1b3e50a646bf - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xaea46a60368a7bd060eec7df8cba43b7ef41ad85 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x6de037ef9ad2725eb40118bb1702ebb27e4aeb24 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xc01154b4ccb518232d6bbfc9b9e6c5068b766f82 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x5a98fcbea516cf06857215779fd812ca3bef1b32 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x102c776ddb30c754ded4fdcc77a19230a60d4e4f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x72e4f9f808c49a2a61de9c5896298920dc4eeea9 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x467719ad09025fcc6cf6f8311755809d45a5e5f3 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xf19308f923582a6f7c465e5ce7a9dc1bec6665b1 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x710287d1d39dcf62094a83ebb3e736e79400068a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xf951e335afb289353dc249e82926178eac7ded78 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xf017d3690346eb8234b85f74cee5e15821fee1f4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x8c282c35b5e1088bb208991c151182a782637699 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xeaa63125dd63f10874f99cdbbb18410e7fc79dd3 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xde342a3e269056fc3305f9e315f4c40d917ba521 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x2dff88a56767223a5529ea5960da7a3f5f766406 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x626e8036deb333b408be468f951bdb42433cbf18 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xdd66781d0e9a08d4fbb5ec7bac80b691be27f21d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xb23d80f5fefcddaa212212f028021b41ded428cf - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xbaac2b4491727d78d2b78815144570b9f2fe8899 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xf8ebf4849f1fa4faf0dff2106a173d3a6cb2eb3a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xb90b2a35c65dbc466b04240097ca756ad2005295 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x1614f18fc94f47967a3fbe5ffcd46d4e7da3d787 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xf1df7305e4bab3885cab5b1e4dfc338452a67891 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x91fbb2503ac69702061f1ac6885759fc853e6eae - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xa9e8acf069c58aec8825542845fd754e41a9489a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x2c95d751da37a5c1d9c5a7fd465c1d50f3d96160 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xe453c3409f8ad2b1fe1ed08e189634d359705a5b - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x89d584a1edb3a70b3b07963f9a3ea5399e38b136 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x4507cef57c46789ef8d1a19ea45f4216bae2b528 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xd1d2eb1b1e90b638588728b4130137d262c87cae - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xe92344b4edf545f3209094b192e46600a19e7c2d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x8a0a9b663693a22235b896f70a229c4a22597623 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x1bbe973bef3a977fc51cbed703e8ffdefe001fed - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xa41d2f8ee4f47d3b860a149765a7df8c3287b7f0 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x761d38e5ddf6ccf6cf7c55759d5210750b5d60f3 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xc18360217d8f7ab5e7c516566761ea12ce7f9d72 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xe28b3b32b6c345a34ff64674606124dd5aceca30 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x168e209d7b2f58f1f24b8ae7b7d35e662bbf11cc - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xb131f4a55907b10d1f0a50d8ab8fa09ec342cd74 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x3472a5a71965499acd81997a54bba8d852c6e53d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x7dd9c5cba05e151c895fde1cf355c9a1d5da6429 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x19efa7d0fc88ffe461d1091f8cbe56dc2708a84f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x14fee680690900ba0cccfc76ad70fd1b95d10e16 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x3c3a81e81dc49a522a592e7622a7e711c06bf354 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xa1290d69c65a6fe4df752f95823fae25cb99e5a7 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x92f419fb7a750aed295b0ddf536276bf5a40124f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x2c06ba9e7f0daccbc1f6a33ea67e85bb68fbee3a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x3d658390460295fb963f54dc0899cfb1c30776df - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x8e870d67f660d95d5be530380d0ec0bd388289e1 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x853d955acef822db058eb8505911ed77f175b99e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x1294f4183763743c7c9519bec51773fb3acd78fd - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x4e15361fd6b4bb609fa63c81a2be19d873717870 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x695d38eb4e57e0f137e36df7c1f0f2635981246b - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x40a7df3df8b56147b781353d379cb960120211d7 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xaaef88cea01475125522e117bfe45cf32044e238 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x163f8c2467924be0ae7b5347228cabf260318753 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x30672ae2680c319ec1028b69670a4a786baa0f35 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xc944e90c64b2c07662a292be6244bdf05cda44a7 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x15e6e0d4ebeac120f9a97e71faa6a0235b85ed12 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x7d225c4cc612e61d26523b099b0718d03152edef - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x82af49447d8a07e3bd95bd0d56f35241523fbab1 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xaf88d065e77c8cc2239327c5edb3a432268e5831 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xff970a61a04b1ca14834a43f5de4533ebddb5cc8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x912ce59144191c1204e64559fe8253a0e49e6548 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x2f2a2543b76a4166549f7aab2e75bef0aefc5b0f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x5979d7b546e38e414f7e9822514be443a4800529 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x35751007a407ca6feffe80b3cb397736d2cf4dbe - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xda10009cbd5d07dd0cecc66161fc93d7c9000da1 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xeb466342c4d449bc9f53a865d5cb90586f405215 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xfc5a1a6eb076a2c7ad06ed22c90d7e710e35ad0a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x0c880f6761f1af8d9aa9c466984b80dab9a8c9e8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xf97f4df75117a78c1a5a0dbb814af92458539fb4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x9623063377ad1b27544c965ccd7342f7ea7e88c7 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x539bde0d7dbd336b79148aa742883198bbf60342 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x3082cc23568ea640225c2467653db90e9250aaa0 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x18c11fd286c5ec11c3b683caa813b77f5163a122 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x289ba1701c2f088cf0faf8b3705246331cb8a839 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x4cb9a7ae498cedcbb5eae9f25736ae7d428c9d66 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x00cbcf7b3d37844e44b888bc747bdd75fcf4e555 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xfa7f8980b0f1e64a2062791cc3b0871572f1f7f0 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xd79bb960dc8a206806c3a428b31bca49934d18d7 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x3096e7bfd0878cc65be71f8899bc4cfb57187ba3 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x13ad51ed4f1b7e9dc168d8a00cb3f4ddd85efa60 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x4e352cf164e64adcbad318c3a1e222e9eba4ce42 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x11cdb42b0eb46d95f990bedd4695a6e3fa034978 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xba5ddd1f9d7f570dc94a51479a000e3bce967196 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xc8ccbd97b96834b976c995a67bf46e5754e2c48e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xd07d35368e04a839dee335e213302b21ef14bb4a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x323665443cef804a3b5206103304bd4872ea4253 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x83d6c8c06ac276465e4c92e7ac8c23740f435140 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x87aaffdf26c6885f6010219208d5b161ec7609c0 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x1b8d516e2146d7a32aca0fcbf9482db85fd42c3a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xafccb724e3aec1657fc9514e3e53a0e71e80622d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x4425742f1ec8d98779690b5a3a6276db85ddc01a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xec70dcb4a1efa46b8f2d97c310c9c4790ba5ffa8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x3419875b4d3bca7f3fdda2db7a476a79fd31b4fe - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x3b60ff35d3f7f62d636b067dd0dc0dfdad670e4e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x58b9cb810a68a7f3e1e4f8cb45d1b9b3c79705e8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xfa5ed56a203466cbbc2430a43c66b9d8723528e7 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x95146881b86b3ee99e63705ec87afe29fcc044d9 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x088cd8f5ef3652623c22d48b1605dcfe860cd704 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xbfd5206962267c7b4b4a8b3d76ac2e1b2a5c4d5e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x6daf586b7370b14163171544fca24abcc0862ac5 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x9d2f299715d94d8a7e6f5eaa8e654e8c74a988a7 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x580e933d90091b9ce380740e3a4a39c67eb85b4c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x655a6beebf2361a19549a99486ff65f709bd2646 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x9e64d3b9e8ec387a9a58ced80b71ed815f8d82b5 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x2297aebd383787a160dd0d9f71508148769342e3 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x6694340fc020c5e6b96567843da2df01b2ce1eb6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x772598e9e62155d7fdfe65fdf01eb5a53a8465be - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x431402e8b9de9aa016c743880e04e517074d8cec - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xd74f5255d557944cf7dd0e45ff521520002d5748 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x6fd58f5a2f3468e35feb098b5f59f04157002407 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x561877b6b3dd7651313794e5f2894b2f18be0766 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xf9ca0ec182a94f6231df9b14bd147ef7fb9fa17c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xd77b108d4f6cefaa0cae9506a934e825becca46e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xd56734d7f9979dd94fae3d67c7e928234e71cd4c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xf1264873436a0771e440e2b28072fafcc5eebd01 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x5575552988a3a80504bbaeb1311674fcfd40ad4b - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x0341c0c0ec423328621788d4854119b97f44e391 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x764bfc309090e7f93edce53e5befa374cdcb7b8e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xaaa6c1e32c55a7bfa8066a6fae9b42650f262418 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x9e20461bc2c4c980f62f1b279d71734207a6a356 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x7fb7ede54259cb3d4e1eaf230c7e2b1ffc951e9a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x3a18dcc9745edcd1ef33ecb93b0b6eba5671e7ca - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x000000000026839b3f4181f2cf69336af6153b99 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x8b0e6f19ee57089f7649a455d89d7bc6314d04e8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x31c91d8fb96bff40955dd2dbc909b36e8b104dde - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x25d887ce7a35172c62febfd67a1856f20faebb00 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xd4d42f0b6def4ce0383636770ef773390d85c61a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xf8388c2b6edf00e2e27eef5200b1befb24ce141d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x619c82392cb6e41778b7d088860fea8447941f4c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x94025780a1ab58868d9b2dbbb775f44b32e8e6e5 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xad4b9c1fbf4923061814dd9d5732eb703faa53d4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xd7a892f28dedc74e6b7b33f93be08abfc394a360 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x3269a3c00ab86c753856fd135d97b87facb0d848 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x4568ca00299819998501914690d6010ae48a59ba - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x21e60ee73f17ac0a411ae5d690f908c3ed66fe12 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xd3188e0df68559c0b63361f6160c57ad88b239d8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x2b41806cbf1ffb3d9e31a9ece6b738bf9d6f645f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xf19547f9ed24aa66b03c3a552d181ae334fbb8db - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x35e6a59f786d9266c7961ea28c7b768b33959cbb - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x59a729658e9245b0cf1f8cb9fb37945d2b06ea27 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xb56c29413af8778977093b9b4947efeea7136c36 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x43ab8f7d2a8dd4102ccea6b438f6d747b1b9f034 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x1d987200df3b744cfa9c14f713f5334cb4bc4d5d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x3404149e9ee6f17fb41db1ce593ee48fbdcd9506 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x080f6aed32fc474dd5717105dba5ea57268f46eb - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xb5a628803ee72d82098d4bcaf29a42e63531b441 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x1622bf67e6e5747b81866fe0b85178a93c7f86e3 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x7dd747d63b094971e6638313a6a2685e80c7fb2e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xa2f9ecf83a48b86265ff5fd36cdbaaa1f349916c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x17a8541b82bf67e10b0874284b4ae66858cb1fd5 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xbcd4d5ac29e06e4973a1ddcd782cd035d04bc0b7 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x42069d11a2cc72388a2e06210921e839cfbd3280 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xbbea044f9e7c0520195e49ad1e561572e7e1b948 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xe85b662fe97e8562f4099d8a1d5a92d4b453bf30 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x3d9907f9a368ad0a51be60f7da3b97cf940982d8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x4e51ac49bc5e2d87e0ef713e9e5ab2d71ef4f336 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x4200000000000000000000000000000000000006 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x7f5c764cbc14f9669b88837ca1490cca17c31607 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x4200000000000000000000000000000000000042 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x0b2c639c533813f4aa9d7837caf62653d097ff85 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x1f32b1c2345538c0c6f582fcb022739c4a194ebb - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x68f180fcce6836688e9084f035309e29bf0a2095 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x94b008aa00579c1307b0ef2c499ad98a8ce58e58 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0xda10009cbd5d07dd0cecc66161fc93d7c9000da1 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0xdc6ff44d5d932cbd77b52e5612ba0529dc6226f1 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x8c6f28f2f1a3c87f0f938b96d27520d9751ec8d9 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x8700daec35af8ff88c16bdf0418774cb3d7599b4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x920cf626a271321c151d027030d5d08af699456b - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x6c84a8f1c29108f47a79964b5fe888d4f4d0de40 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x9e1028f5f1d5ede59748ffcee5532509976840e0 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0xeb466342c4d449bc9f53a865d5cb90586f405215 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x350a791bfc2c21f9ed5d10980dad2e2638ffa7f6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x17aabf6838a6303fc6e9c5a227dc1eb6d95c829a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0xf467c7d5a4a9c4687ffc7986ac6ad5a4c81e1404 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x76fb31fb4af56892a25e32cfc43de717950c9278 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0xc5b001dc33727f8f26880b184090d3e252470d45 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x9560e827af36c94d2ac33a39bce1fe78631088db - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x9bcef72be871e61ed4fbbc7630889bee758eb81d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x50c5725949a6f0c72e6c4a641f24049a917db0cb - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0xf98dcd95217e15e05d8638da4c91125e59590b07 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x4b03afc91295ed778320c2824bad5eb5a1d852dd - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0xc40f949f8a4e094d1b49a23ea9241d289b7b2819 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x323665443cef804a3b5206103304bd4872ea4253 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x50bce64397c75488465253c0a034b8097fea6578 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x296f55f8fb28e498b858d0bcda06d955b2cb3f97 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x2598c30330d5771ae9f983979209486ae26de875 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x0994206dfe8de6ec6920ff4d779b0d950605fb53 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0xc3248a1bd9d72fa3da6e6ba701e58cbf818354eb - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x6fd9d7ad17242c41f7131d257212c54a0e816691 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x14778860e937f509e651192a90589de711fb88a9 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0xdfa46478f9e5ea86d57387849598dbfb2e964b02 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x9b88d293b7a791e40d36a39765ffd5a1b9b5c349 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x3eb398fec5f7327c6b15099a9681d9568ded2e82 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x217d47011b23bb961eb6d93ca9945b7501a5bb11 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0xbfd5206962267c7b4b4a8b3d76ac2e1b2a5c4d5e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x1cef2d62af4cd26673c7416957cc4ec619a696a7 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x9fd22a17b4a96da3f83797d122172c450381fb88 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0xaddb6a0412de1ba0f936dcaeb8aaa24578dcf3b2 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x2791bca1f2de4661ed88a30c99a7a9449aa84174 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x7ceb23fd6bc0add59e62ac25578270cff1b9f619 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x3c499c542cef5e3811e1192ce70d8cc03d5c3359 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x1bfd67037b42cf73acf2047067bd4f2c47d9bfd6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xc2132d05d31c914a87c6611c10748aeb04b58e8f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x53e0bca35ec356bd5dddfebbd1fc0fd03fabad39 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x61299774020da444af134c82fa83e3810b309991 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xd6df932a45c0f255f85145f286ea0b292b21c90b - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x2ad2934d5bfb7912304754479dd1f096d5c807da - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xc3c7d422809852031b44ab29eec9f1eff2a58756 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x8f3cf7ad23cd3cadbd9735aff958023239c6a063 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x750e4c4984a9e0f12978ea6742bc1c5d248f40ed - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x111111517e4929d3dcbdfa7cce55d30d4b6bc4d6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xd0258a3fd00f38aa8090dfee343f10a9d4d30d3f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x430ef9263e76dae63c84292c3409d61c598e9682 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xb33eaad8d922b1083446dc23f610c2567fb5180f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xdc3326e71d45186f113a2f448984ca0e8d201995 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x311434160d7537be358930def317afb606c0d737 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x0b3f868e0be5597d5db7feb59e1cadbb0fdda50a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xe3f2b1b2229c0333ad17d03f179b87500e7c5e01 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xac0f66379a6d7801d7726d5a943356a172549adb - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xf88332547c680f755481bf489d890426248bb275 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xe5417af564e4bfda1c483642db72007871397896 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xe261d618a959afffd53168cd07d12e37b26761db - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xe0b52e49357fd4daf2c15e02058dce6bc0057db4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xbbba073c31bf03b8acf7c28ef0738decf3695683 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xe238ecb42c424e877652ad82d8a939183a04c35f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x3b56a704c01d650147ade2b8cee594066b3f9421 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x5fe2b58c013d7601147dcdd68c143a77499f5531 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x172370d5cd63279efa6d502dab29171933a610af - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x53df32548214f51821cf1fe4368109ac5ddea1ff - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xff76c0b48363a7c7307868a81548d340049b0023 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x6f8a06447ff6fcf75d803135a7de15ce88c1d4ec - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x50b728d8d964fd00c2d0aad81718b71311fef68a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x3a58a54c066fdc0f2d55fc9c89f0415c92ebf3c4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x03b54a6e9a984069379fae1a4fc4dbae93b3bccd - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xd93f7e271cb87c23aaa73edc008a79646d1f9912 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x200c234721b5e549c3693ccc93cf191f90dc2af9 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x11cd37bb86f65419713f30673a480ea33c826872 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x8a16d4bf8a0a716017e8d2262c4ac32927797a2f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x9a71012b13ca4d3d0cdc72a177df3ef03b0e76a3 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xa1c57f48f0deb89f569dfbe6e2b7f46d33606fd4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x190eb8a183d22a4bdf278c6791b152228857c033 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x2f6f07cdcf3588944bf4c42ac74ff24bf56e7590 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x235737dbb56e8517391473f7c964db31fa6ef280 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x0b220b82f3ea3b7f6d9a1d8ab58930c064a2b5bf - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x8bff1bd27e2789fe390acabc379c380a83b68e84 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xb58458c52b6511dc723d7d6f3be8c36d7383b4a8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x323665443cef804a3b5206103304bd4872ea4253 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x2760e46d9bb43dafcbecaad1f64b93207f9f0ed7 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x18ec0a6e18e5bc3784fdd3a3634b31245ab704f6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x431d5dff03120afa4bdf332c61a6e1766ef37bdb - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x6f7c932e7684666c9fd1d44527765433e01ff61d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xeee3371b89fc43ea970e908536fcddd975135d8a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xe5b49820e5a1063f6f4ddf851327b5e8b2301048 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xaa3717090cddc9b227e49d0d84a28ac0a996e6ff - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x62a872d9977db171d9e213a5dc2b782e72ca0033 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x381caf412b45dac0f62fbeec89de306d3eabe384 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xe0bceef36f3a6efdd5eebfacd591423f8549b9d5 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x23d29d30e35c5e8d321e1dc9a8a61bfd846d4c5c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x282d8efce846a88b159800bd4130ad77443fa1a1 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x74dd45dd579cad749f9381d6227e7e02277c944b - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x714db550b574b3e927af3d93e26127d15721d4c2 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xfa68fb4628dff1028cfec22b4162fccd0d45efb6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xe631dabef60c37a37d70d3b4f812871df663226f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xdb725f82818de83e99f1dac22a9b5b51d3d04dd4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x3c59798620e5fec0ae6df1a19c6454094572ab92 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x0d0b8488222f7f83b23e365320a4021b12ead608 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xa380c0b01ad15c8cf6b46890bddab5f0868e87f3 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x8a953cfe442c5e8855cc6c61b1293fa648bae472 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x45c32fa6df82ead1e2ef74d17b76547eddfaff89 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x11cd72f7a4b699c67f225ca8abb20bc9f8db90c7 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x0c9c7712c83b3c70e7c5e11100d33d9401bdf9dd - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x77a6f2e9a9e44fd5d5c3f9be9e52831fc1c3c0a0 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xbfc70507384047aa74c29cdc8c5cb88d0f7213ac - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xfcb54da3f4193435184f3f647467e12b50754575 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x9a6a40cdf21a0af417f1b815223fd92c85636c58 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xe111178a87a3bff0c8d18decba5798827539ae99 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x82617aa52dddf5ed9bb7b370ed777b3182a30fd1 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x2ab0e9e4ee70fff1fb9d67031e44f6410170d00e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xa486c6bc102f409180ccb8a94ba045d39f8fc7cb - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xc4a206a306f0db88f98a3591419bc14832536862 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xf0059cc2b3e980065a906940fbce5f9db7ae40a7 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x16eccfdbb4ee1a85a33f3a9b21175cd7ae753db4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x553d3d295e0f695b9228246232edf400ed3560b5 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x14af1f2f02dccb1e43402339099a05a5e363b83c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x7bdf330f423ea880ff95fc41a280fd5ecfd3d09f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x8505b9d2254a7ae468c0e9dd10ccea3a837aef5c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xe2aa7db6da1dae97c5f5c6914d285fbfcc32a128 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xb7b31a6bc18e48888545ce79e83e06003be70930 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x1631244689ec1fecbdd22fb5916e920dfc9b8d30 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xf6372cdb9c1d3674e83842e3800f2a62ac9f3c66 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x692ac1e363ae34b6b489148152b12e2785a3d8d6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x0266f4f08d82372cf0fcbccc0ff74309089c74d1 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x7fbc10850cae055b27039af31bd258430e714c62 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xa3fa99a148fa48d14ed51d610c367c61876997f1 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x9dbfc1cbf7a1e711503a29b4b5f9130ebeccac96 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x236aa50979d5f3de3bd1eeb40e81137f22ab794b - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xf86df9b91f002cfeb2aed0e6d05c4c4eaef7cf02 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x4200000000000000000000000000000000000006 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x6921b130d297cc43754afba22e5eac0fbf8db75b - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x5babfc2f240bc5de90eb7e19d789412db1dec402 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x532f27101965dd16442e59d40670faf5ebb142e4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x833589fcd6edb6e08f4c7c32d4f71b54bda02913 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x4ed4e862860bed51a9570b96d89af5e1b0efefed - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x0d97f261b1e88845184f678e2d1e7a98d9fd38de - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x8129b94753f22ec4e62e2c4d099ffe6773969ebc - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x3f14920c99beb920afa163031c4e47a3e03b3e4a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x940181a94a35a4569e4529a3cdfb74e38fd98631 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x3419875b4d3bca7f3fdda2db7a476a79fd31b4fe - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0xa067436db77ab18b1a315095e4b816791609897c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0xafb89a09d82fbde58f18ac6437b3fc81724e4df6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x489fe42c267fe0366b16b0c39e7aeef977e841ef - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0xdc46c1e93b71ff9209a0f8076a9951569dc35855 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x91f45aa2bde7393e0af1cc674ffe75d746b93567 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x236aa50979d5f3de3bd1eeb40e81137f22ab794b - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0xf6e932ca12afa26665dc4dde7e27be02a7c02e50 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x524d524b4c9366be706d3a90dcf70076ca037ae3 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x5b5dee44552546ecea05edea01dcd7be7aa6144a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x2598c30330d5771ae9f983979209486ae26de875 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0xfa980ced6895ac314e7de34ef1bfae90a5add21b - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x469fda1fb46fcb4befc0d8b994b516bd28c87003 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x4e496c0256fb9d4cc7ba2fdf931bc9cbb7731660 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x27d2decb4bfc9c76f0309b8e88dec3a601fe25a8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0xbfd5206962267c7b4b4a8b3d76ac2e1b2a5c4d5e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x9e1028f5f1d5ede59748ffcee5532509976840e0 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x3c3aa127e6ee3d2f2e432d0184dd36f2d2076b52 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0xba5e6fa2f33f3955f0cef50c63dcc84861eab663 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x97c806e7665d3afd84a8fe1837921403d59f3dcc - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x8ee73c484a26e0a5df2ee2a4960b789967dd0415 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x00e57ec29ef2ba7df07ad10573011647b2366f6d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x8f019931375454fe4ee353427eb94e2e0c9e0a8c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x93e6407554b2f02640ab806cd57bd83e848ec65d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x55d398326f99059ff775485246999027b3197955 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x2170ed0880ac9a755fd29b2688956bd959f933f8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xfdc66a08b0d0dc44c17bbd471b88f49f50cdd20f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x7130d2a12b9bcbfae4f2634d864a1ee1ce3ead9c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x1d2f0da169ceb9fc7b3144628db156f3f6c60dbe - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xe9e7cea3dedca5984780bafc599bd69add087d56 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xfa54ff1a158b5189ebba6ae130ced6bbd3aea76e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x570a5d26f7765ecb712c0924e4de545b89fd43df - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x47c454ca6be2f6def6f32b638c80f91c9c3c5949 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xad86d0e9764ba90ddd68747d64bffbd79879a238 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xf8a0bf9cf54bb92f17374d9e9a321e6a111a51bd - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xd691d9a68c887bdf34da8c36f63487333acfd103 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x1af3f329e8be154074d8769d1ffa4ee058b1dbc3 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x1294f4183763743c7c9519bec51773fb3acd78fd - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xb04906e95ab5d797ada81508115611fee694c2b3 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x111111111117dc0aa78b770fa6a738034120c302 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xcc42724c6683b7e57334c4e856f4c9965ed682bd - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x90c97f71e18723b0cf0dfa30ee176ab653e89f40 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x0e09fabb73bd3ade0a17ecc321fd13a19e81ce82 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x2b72867c32cf673f7b02d208b26889fed353b1f8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x031b41e504677879370e9dbcf937283a8691fa7f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x1ce0c2827e2ef14d5c4f29a091d735a204794041 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xcf3bb6ac0f6d987a5727e2d15e39c2d6061d5bec - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x8ff795a6f4d97e7887c79bea79aba5cc76444adf - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x2dff88a56767223a5529ea5960da7a3f5f766406 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x003d87d02a2a01e9e8a20f507c83e15dd83a33d1 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x4b0f1812e5df2a09796481ff14017e6005508003 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xbf5140a22578168fd562dccf235e5d43a02ce9b1 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xca1c644704febf4ab81f85daca488d1623c28e63 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x51e72dd1f2628295cc2ef931cb64fdbdc3a0c599 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xbbca42c60b5290f2c48871a596492f93ff0ddc82 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x555296de6a86e72752e5c5dc091fe49713aa145c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x0808bf94d57c905f1236212654268ef82e1e594e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x8457ca5040ad67fdebbcc8edce889a335bc0fbfb - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xcebef3df1f3c5bfd90fde603e71f31a53b11944d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x90ed8f1dc86388f14b64ba8fb4bbd23099f18240 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x9840652dc04fb9db2c43853633f0f62be6f00f98 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xba2ae424d960c26247dd6c32edc70b295c744c43 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x0782b6d8c4551b9760e74c0545a9bcd90bdc41e5 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xbe2b6c5e31f292009f495ddbda88e28391c9815e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x8f0528ce5ef7b51152a59745befdd91d97091d2f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xffeecbf8d7267757c2dc3d13d730e97e15bfdf7f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x0eb3a705fc54725037cc9e008bdede697f62f335 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xf21768ccbc73ea5b6fd3c687208a7c2def2d966e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x0000028a2eb8346cd5c0267856ab7594b7a55308 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x76a797a59ba2c17726896976b7b3747bfd1d220f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xc79d1fd14f514cd713b5ca43d288a782ae53eab2 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xad29abb318791d579433d831ed122afeaf29dcfe - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x3203c9e46ca618c8c1ce5dc67e7e9d75f5da2377 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xdb021b1b247fe2f1fa57e0a87c748cc1e321f07f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x7083609fce4d1d8dc0c979aab8c869ea2c873402 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xc5f0f7b66764f6ec8c8dff7ba683102295e16409 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xe29142e14e52bdfbb8108076f66f49661f10ec10 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xb0d502e938ed5f4df2e681fe6e419ff29631d62b - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x6730f7a6bbb7b9c8e60843948f7feb4b6a17b7f7 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x1613957159e9b0ac6c80e824f7eea748a32a0ae2 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/celo/0x471ece3750da237f93b8e339c536989b8978a438 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/celo/0x765de816845861e75a25fca122bb6898b8b1282a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/celo/0x66803fb87abd4aac3cbb3fad7c3aa01f6f3fb207 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/celo/0xd8763cba276a3738e6de85b4b3bf5fded6d6ca73 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/celo/0x37f750b7cc259a2f741af45294f6a16572cf5cad - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/celo/0xd71ffd0940c920786ec4dbb5a12306669b5b81ef - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/celo/0xe8537a3d056da446677b9e9d6c5db704eaab4787 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/celo/0x4f604735c1cf31399c6e711d5962b2b3e0225ad3 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/celo/0x02de4766c272abc10bc88c220d214a26960a7e92 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/celo/0xceba9300f2b948710d2653dd7b07f33a8b32118c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/celo/0xc16b81af351ba9e64c1a069e3ab18c244a1e3049 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x728f30fa2f100742c7949d1961804fa8e0b1387d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x41ea5d41eeacc2d5c4072260945118a13bb7ebce - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xf21661d0d1d76d3ecb8e1b9f1c923dbfffae4097 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xb0ecc6ac0073c063dcfc026ccdc9039cae2998e1 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x00f932f0fe257456b32deda4758922e56a4f4b42 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0xa4af354d466e8a68090dd9eb2cb7caf162f4c8c2 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xba50933c268f567bdc86e1ac131be072c6b0b71a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xd29da236dd4aac627346e1bba06a619e8c22d7c5 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x1bfce574deff725a3f483c334b790e25c8fa9779 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x9e18d5bab2fa94a6a95f509ecb38f8f68322abd3 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xcd5fe23c85820f7b72d0926fc9b05b43e359b7ee - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xbf5495efe5db9ce00f80364c8b423567e58d2110 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x065b4e5dfd50ac12a81722fd0a0de81d78ddf7fb - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x57e114b691db790c35207b2e685d4a43181e6061 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x0b7f0e51cd1739d6c96982d55ad8fa634dd43a9c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xc56c7a0eaa804f854b536a5f3d5f49d2ec4b12b8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x594daad7d77592a2b97b725a7ad59d7e188b5bfa - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x8355dbe8b0e275abad27eb843f3eaf3fc855e525 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x2a961d752eaa791cbff05991e4613290aec0d9ac - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x38e68a37e401f7271568cecaac63c6b1e19130b4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x1131d427ecd794714ed00733ac0f851e904c8398 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x1495bc9e44af1f8bcb62278d2bec4540cf0c05ea - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x808507121b80c02388fad14726482e061b8da827 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x44971abf0251958492fee97da3e5c5ada88b9185 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x320623b8e4ff03373931769a31fc52a4e78b5d70 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x6e5970dbd6fc7eb1f29c6d2edf2bc4c36124c0c1 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xd40c688da9df74e03566eaf0a7c754ed98fbb8cc - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x8afe4055ebc86bd2afb3940c0095c9aca511d852 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x9ce84f6a69986a83d92c324df10bc8e64771030f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xbe4d9c8c638b5f0864017d7f6a04b66c42953847 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x68bbed6a47194eff1cf514b50ea91895597fc91e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x69420e3a3aa9e17dea102bb3a9b3b73dcddb9528 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x7420b4b9a0110cdc71fb720908340c03f9bc03ec - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x03aa6298f1370642642415edc0db8b957783e8d6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xd533a949740bb3306d119cc777fa900ba034cd52 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xf14dd7b286ce197019cba54b189d2b883e70f761 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xa35923162c49cf95e6bf26623385eb431ad920d3 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x8cefbeb2172a9382753de431a493e21ba9694004 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x120a3879da835a5af037bb2d1456bebd6b54d4ba - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x69457a1c9ec492419344da01daf0df0e0369d5d0 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xf6ce4be313ead51511215f1874c898239a331e37 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x73d7c860998ca3c01ce8c808f5577d94d545d1b4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xeff49b0f56a97c7fd3b51f0ecd2ce999a7861420 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x236501327e701692a281934230af0b6be8df3353 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x5026f006b85729a8b14553fae6af249ad16c9aab - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x66761fa41377003622aee3c7675fc7b5c1c2fac5 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x9f9c8ec3534c3ce16f928381372bfbfbfb9f4d24 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xd8c978de79e12728e38aa952a6cb4166f891790f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x7122985656e38bdc0302db86685bb972b145bd3c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x582d872a1b094fc48f5de31d3b73f2d9be47def1 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x504624040e0642921c2c266a9ac37cafbd8cda4e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xc548e90589b166e1364de744e6d35d8748996fe8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x4c11249814f11b9346808179cf06e71ac328c1b5 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x423f4e6138e475d85cf7ea071ac92097ed631eea - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x8390a1da07e376ef7add4be859ba74fb83aa02d5 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xf94e7d0710709388bce3161c32b4eea56d3f91cc - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xaa95f26e30001251fb905d264aa7b00ee9df6c18 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x2416092f143378750bb29b79ed961ab195cceea5 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x6c84a8f1c29108f47a79964b5fe888d4f4d0de40 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x71eeba415a523f5c952cc2f06361d5443545ad28 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x88a269df8fe7f53e590c561954c52fccc8ec0cfb - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x429fed88f10285e61b12bdf00848315fbdfcc341 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xb299751b088336e165da313c33e3195b8c6663a6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xf0a479c9c3378638ec603b8b6b0d75903902550b - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xb59c8912c83157a955f9d715e556257f432c35d7 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xba0dda8762c24da9487f5fa026a9b64b695a07ea - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xc24a365a870821eb83fd216c9596edd89479d8d7 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xa586b3b80d7e3e8d439e25fbc16bc5bcee3e2c85 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xef04804e1e474d3f9b73184d7ef5d786f3fce930 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x2e9a6df78e42a30712c10a9dc4b1c8656f8f2879 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x13a7dedb7169a17be92b0e3c7c2315b46f4772b3 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x1dd6b5f9281c6b4f043c02a83a46c2772024636c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xc5102fe9359fd9a28f877a67e36b0f050d81a3cc - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xf525e73bdeb4ac1b0e741af3ed8a8cbb43ab0756 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xe4177c1400a8eee1799835dcde2489c6f0d5d616 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xed5740209fcf6974d6f3a5f11e295b5e468ac27c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xe10d4a4255d2d35c9e23e2c4790e073046fbaf5c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x10398abc267496e49106b07dd6be13364d10dc71 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x2218a117083f5b482b0bb821d27056ba9c04b1d3 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x395ae52bb17aef68c2888d941736a71dc6d4e125 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x9a601c5bb360811d96a23689066af316a30c3027 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xbac3368b5110f3a3dda8b5a0f7b66edb37c47afe - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x1d3c629ca5c1d0ab3bdf74600e81b4145615df8e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xe9c21de62c5c5d0ceacce2762bf655afdceb7ab3 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x658cda444ac43b0a7da13d638700931319b64014 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x3d2bd0e15829aa5c362a4144fdf4a1112fa29b5c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x3fb83a9a2c4408909c058b0bfe5b4823f54fafe2 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x00e5646f60ac6fb446f621d146b6e1886f002905 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x12a4cebf81f8671faf1ab0acea4e3429e42869e7 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x9ff62d1fc52a907b6dcba8077c2ddca6e6a9d3e1 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xc61f39418cd27820b5d4e9ba4a7197eefaeb8b05 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x15b7c0c907e4c6b9adaaaabc300c08991d6cea05 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x7f67639ffc8c93dd558d452b8920b28815638c44 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x276c9cbaa4bdf57d7109a41e67bd09699536fa3d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x041fdf3f472d2c8a7ecc458fc3b7f543e6c57ef7 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x3c281a39944a2319aa653d81cfd93ca10983d234 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x96419929d7949d6a801a6909c145c8eef6a40431 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0xfea9dcdc9e23a9068bf557ad5b186675c61d33ea - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0xdb6e0e5094a25a052ab6845a9f1e486b9a9b3dde - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0xcde172dc5ffc46d228838446c57c1227e0b82049 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0xff0c532fdb8cd566ae169c1cb157ff2bdc83e105 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x9a26f5433671751c3276a065f57e5a02d2817973 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x3636a7734b669ce352e97780df361ce1f809c58c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x50c5725949a6f0c72e6c4a641f24049a917db0cb - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0xe3086852a4b125803c815a158249ae468a3254ca - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0xbeb0fd48c2ba0f1aacad2814605f09e08a96b94e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0xbc45647ea894030a4e9801ec03479739fa2485f0 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x768be13e1680b5ebe0024c42c896e3db59ec0149 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x928a6a9fc62b2c94baf2992a6fba4715f5bb0066 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0xbf4db8b7a679f89ef38125d5f84dd1446af2ea3b - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0xed899bfdb28c8ad65307fa40f4acab113ae2e14c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x1b6a569dd61edce3c383f6d565e2f79ec3a12980 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x76734b57dfe834f102fb61e1ebf844adf8dd931e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x4621b7a9c75199271f773ebd9a499dbd165c3191 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0xaf07d812d1dcec20bf741075bc18660738d226dd - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x7f12d13b34f5f4f0a9449c16bcd42f0da47af200 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x55a6f6cb50db03259f6ab17979a4891313be2f45 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x968d6a288d7b024d5012c0b25d67a889e4e3ec19 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x7a8a5012022bccbf3ea4b03cd2bb5583d915fb1a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0xcde90558fc317c69580deeaf3efc509428df9080 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x0028e1e60167b48a938b785aa5292917e7eaca8b - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x76e7447bafa3f0acafc9692629b1d1bc937ca15d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x15ac90165f8b45a80534228bdcb124a011f62fee - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x4045b33f339a3027af80013fb5451fdbb01a4492 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0xddf98aad8180c3e368467782cd07ae2e3e8d36a5 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x698dc45e4f10966f6d1d98e3bfd7071d8144c233 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x3c8665472ec5af30981b06b4e0143663ebedcc1e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x18a8bd1fe17a1bb9ffb39ecd83e9489cfd17a022 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0xba0dda8762c24da9487f5fa026a9b64b695a07ea - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x13741c5df9ab03e7aa9fb3bf1f714551dd5a5f8a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0xebff2db643cf955247339c8c6bcd8406308ca437 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0xfadb26be94c1f959f900bf88cd396b3e803481d6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x52c2b317eb0bb61e650683d2f287f56c413e4cf6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x38d513ec43dda20f323f26c7bef74c5cf80b6477 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x33ad778e6c76237d843c52d7cafc972bb7cf8729 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x290814ad0fbd2b935f34d7b40306102313d4c63e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x5e432eecd01c12ee7071ee9219c2477a347da192 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0xbdf5bafee1291eec45ae3aadac89be8152d4e673 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0xff62ddfa80e513114c3a0bf4d6ffff1c1d17aadf - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x8c81b4c816d66d36c4bf348bdec01dbcbc70e987 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x6b82297c6f1f9c3b1f501450d2ee7c37667ab70d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x42069babe14fb1802c5cb0f50bb9d2ad6fef55e2 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x72499bddb67f4ca150e1f522ca82c87bc9fb18c8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x0578d8a44db98b23bf096a382e016e29a5ce0ffe - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x8fe815417913a93ea99049fc0718ee1647a2a07c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x7d12aeb5d96d221071d176980d23c213d88d9998 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0xb166e8b140d35d9d8226e40c09f757bac5a4d87d - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x8853f0c059c27527d33d02378e5e4f6d5afb574a - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0xf3c052f2baab885c610a748eb01dfbb643ba835b - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0xcd1cffa8ebc66f1a2cf7675b48ba955ffcb82d8e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0xde7a416ac821c77478340eebaa21b68297025ef3 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x2da56acb9ea78330f947bd57c54119debda7af71 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x8972ab69d499b5537a31576725f0af8f67203d38 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x88faea256f789f8dd50de54f9c807eef24f71b16 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x42069de48741db40aef864f8764432bbccbd0b69 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x9a27c6759a6de0f26ac41264f0856617dec6bc3f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0xfaa4f3bcfc87d791e9305951275e0f62a98bcb10 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0xfd9fa4f785331ce88b5af8994a047ba087c705d8 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x21eceaf3bf88ef0797e3927d855ca5bb569a47fc - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x7d9ce55d54ff3feddb611fc63ff63ec01f26d15f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x4229c271c19ca5f319fb67b4bc8a40761a6d6299 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x80f45eacf6537498ecc660e4e4a2d2f99e195cf4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x1a475d06d967aeb686c98de80d079d72097aeacf - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x4fb9b20dafe45d91ae287f2e07b2e79709308178 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0xd3741ac9b3f280b0819191e4b30be4ecd990771e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x09579452bc3872727a5d105f342645792bb8a82b - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x8a24d7260cd02d3dfd8eefb66bc17ad4b17d494c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0xd88611a629265c9af294ffdd2e7fa4546612273e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x9a86980d3625b4a6e69d8a4606d51cbc019e2002 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x1c7a460413dd4e964f96d8dfc56e7223ce88cd85 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x776aaef8d8760129a0398cf8674ee28cefc0eab9 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x28e29ec91db66733a94ee8e3b86a6199117baf99 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0xb9898511bd2bad8bfc23eba641ef97a08f27e730 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x76baa16ff15d61d32e6b3576c3a8c83a25c2f180 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x2816a491dd0b7a88d84cbded842a618e59016888 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0xa7ea9d5d4d4c7cf7dbde5871e6d108603c6942a5 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/base/0x586e10db93630a4d2da6c6a34ba715305b556f04 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xf486ad071f3bee968384d2e39e2d8af0fcf6fd46 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x76d36d44dc4595e8d2eb3ad745f175eda134284f - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x1fa4a73a3f0133f0025378af00236f3abdee5d63 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xb3ed0a426155b79b898849803e3b36552f7ed507 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x0ef4a107b48163ab4b57fca36e1352151a587be4 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x62694d43ccb9b64e76e38385d15e325c7712a735 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xa2b726b1145a4773f68593cf171187d8ebe4d495 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xf275e1ac303a4c9d987a2c48b8e555a77fec3f1c - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x11a31b833d43853f8869c9eec17f60e3b4d2a753 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 https://app.uniswap.org/explore/tokens/celo/0x48065fbbe25f71c9282ddf5e1cd6d6a887483d5e - 2025-10-17T22:04:33.647Z + 2025-03-20T21:19:45.690Z 0.8 - https://app.uniswap.org/explore/tokens/ethereum/0xbadff0ef41d2a68f22de21eabca8a59aaf495cf0 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/tokens/ethereum/NATIVE + 2025-03-20T21:28:30.528Z 0.8 - https://app.uniswap.org/explore/tokens/ethereum/0x1fdd61ef9a5c31b9a2abc7d39c139c779e8412af - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/tokens/base/NATIVE + 2025-03-20T21:28:30.528Z 0.8 - https://app.uniswap.org/explore/tokens/ethereum/0x4ade2b180f65ed752b6f1296d0418ad21eb578c0 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/tokens/bnb/NATIVE + 2025-03-20T21:28:30.528Z 0.8 - https://app.uniswap.org/explore/tokens/ethereum/0x0c5cb676e38d6973837b9496f6524835208145a2 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/tokens/ethereum/0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf + 2025-03-20T21:28:30.528Z 0.8 - https://app.uniswap.org/explore/tokens/ethereum/0xb69753c06bb5c366be51e73bfc0cc2e3dc07e371 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/tokens/base/0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf + 2025-03-20T21:28:30.528Z 0.8 - https://app.uniswap.org/explore/tokens/ethereum/0x8143182a775c54578c8b7b3ef77982498866945d - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/tokens/ethereum/0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9 + 2025-03-20T21:28:30.528Z 0.8 - https://app.uniswap.org/explore/tokens/ethereum/0x76e222b07c53d28b89b0bac18602810fc22b49a8 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/tokens/ethereum/0x73a15fed60bf67631dc6cd7bc5b6e8da8190acf5 + 2025-03-20T21:28:30.528Z 0.8 - https://app.uniswap.org/explore/tokens/ethereum/0x18aaa7115705e8be94bffebde57af9bfc265b998 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/tokens/ethereum/0x18084fba666a33d37592fa2633fd49a74dd93a88 + 2025-03-20T21:28:30.528Z 0.8 - https://app.uniswap.org/explore/tokens/ethereum/0x7d8146cf21e8d7cbe46054e01588207b51198729 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/tokens/ethereum/0x8236a87084f8b84306f72007f36f2618a5634494 + 2025-03-20T21:28:30.528Z 0.8 - https://app.uniswap.org/explore/tokens/ethereum/0xfe0c30065b384f05761f15d0cc899d4f9f9cc0eb - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/tokens/optimism/NATIVE + 2025-03-20T21:28:30.528Z 0.8 - https://app.uniswap.org/explore/tokens/ethereum/0x1ce270557c1f68cfb577b856766310bf8b47fd9c - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/tokens/ethereum/0x26e550ac11b26f78a04489d5f20f24e3559f7dd9 + 2025-03-20T21:28:30.528Z 0.8 - https://app.uniswap.org/explore/tokens/ethereum/0x793a5d8b30aab326f83d20a9370c827fea8fdc51 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/tokens/polygon/0x0000000000000000000000000000000000001010 + 2025-03-20T21:28:30.528Z 0.8 - https://app.uniswap.org/explore/tokens/ethereum/0xff836a5821e69066c87e268bc51b849fab94240c - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/tokens/ethereum/0x9d39a5de30e57443bff2a8307a4256c8797a3497 + 2025-03-20T21:28:30.528Z 0.8 - https://app.uniswap.org/explore/tokens/ethereum/0xf4d2888d29d722226fafa5d9b24f9164c092421e - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/tokens/ethereum/0x4c1746a800d224393fe2470c70a35717ed4ea5f1 + 2025-03-20T21:28:30.528Z 0.8 - https://app.uniswap.org/explore/tokens/ethereum/0x8ed97a637a790be1feff5e888d43629dc05408f6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/tokens/base/0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b + 2025-03-20T21:28:30.528Z 0.8 - https://app.uniswap.org/explore/tokens/ethereum/0x31c8eacbffdd875c74b94b077895bd78cf1e64a3 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/tokens/arbitrum/NATIVE + 2025-03-20T21:28:30.528Z 0.8 - https://app.uniswap.org/explore/tokens/ethereum/0xc55126051b22ebb829d00368f4b12bde432de5da - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/tokens/ethereum/0x3593d125a4f7849a1b059e64f4517a86dd60c95d + 2025-03-20T21:28:30.528Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xe0f63a424a4439cbe457d80e4f4b51ad25b2c56c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x8881562783028f5c1bcb985d2283d5e170d88888 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x67466be17df832165f8c80a5a120ccc652bd7e69 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xd939212f16560447ed82ce46ca40a63db62419b5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x88417754ff7062c10f4e3a4ab7e9f9d9cbda6023 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x5afe3855358e112b5647b952709e6165e1c1eeee - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x02e7f808990638e9e67e1f00313037ede2362361 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xd2bdaaf2b9cc6981fd273dcb7c04023bfbe0a7fe - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x112b08621e27e10773ec95d250604a041f36c582 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x32b053f2cba79f80ada5078cb6b305da92bde6e1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x5ac34c53a04b9aaa0bf047e7291fb4e8a48f2a18 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x26ebb8213fb8d66156f1af8908d43f7e3e367c1d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xe3b9cfb8ea8a4f1279fbc28d3e15b4d2d86f18a0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x8207c1ffc5b6804f6024322ccf34f29c3541ae26 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x255f1b39172f65dc6406b8bee8b08155c45fe1b6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x092baadb7def4c3981454dd9c0a0d7ff07bcfc86 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x53bcf6698c911b2a7409a740eacddb901fc2a2c6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x2ac2b254bc18cd4999f64773a966e4f4869c34ee - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x17fc002b466eec40dae837fc4be5c67993ddbd6f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0xc8a4eea31e9b6b61c406df013dd4fec76f21e279 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x498bf2b1e120fed3ad3d42ea2165e9b73f99c1e5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0xe4dddfe67e7164b0fe14e218d80dc4c08edc01cb - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x7c8a1a80fdd00c9cccd6ebd573e9ecb49bfa2a59 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x1debd73e752beaf79865fd6446b0c970eae7732f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0xaf5db6e1cc585ca312e8c8f7c499033590cf5c98 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/optimism/0x65559aa14915a70190438ef90104769e5e890a00 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/optimism/0x7fb688ccf682d58f86d7e38e03f9d22e7705448b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/optimism/0x73cb180bf0521828d8849bc8cf2b920918e23032 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/optimism/0x2e3d870790dc77a83dd1d18184acc7439a53f475 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/optimism/0xa00e3a3511aac35ca78530c85007afcd31753819 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/optimism/0x528cdc92eab044e1e39fe43b9514bfdab4412b98 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/optimism/0x4f604735c1cf31399c6e711d5962b2b3e0225ad3 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x1c954e8fe737f99f68fa1ccda3e51ebdb291948c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0xf50d05a1402d0adafa880d36050736f9f6ee7dee - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0xab0b2ddb9c7e440fac8e140a89c0dbcbf2d7bbff - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x8bc3ec2e7973e64be582a90b08cadd13457160fe - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x64060ab139feaae7f06ca4e63189d86adeb51691 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x5ec03c1f7fa7ff05ec476d19e34a22eddb48acdc - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x9627a3d6872be48410fcece9b1ddd344bf08c53e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x1ed02954d60ba14e26c230eec40cbac55fa3aeea - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x8d3419b9a18651f3926a205ee0b1acea1e7192de - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xb56d0839998fd79efcd15c27cf966250aa58d6d3 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x81f91fe59ee415735d59bd5be5cca91a0ea4fa69 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x87c211144b1d9bdaa5a791b8099ea4123dc31d21 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xf4210f93bc68d63df3286c73eba08c6414f40c0d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xece7b98bd817ee5b1f2f536daf34d0b6af8bb542 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x4c96a67b0577358894407af7bc3158fc1dffbeb5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x70737489dfdf1a29b7584d40500d3561bd4fe196 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x39353a32eceafe4979a8606512c046c3b6398cc4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x92fb1b7d9730b2f1bd4e2e91368c1eb6fdd2a009 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x174e33ef2effa0a4893d97dda5db4044cc7993a3 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xfdc944fb59201fb163596ee5e209ebc8fa4dcdc5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x388e543a5a491e7b42e3fbcd127dd6812ea02d0d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x56a38e7216304108e841579041249feb236c887b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x1804e3db872eed4141e482ff74c56862f2791103 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x9de16c805a3227b9b92e39a446f9d56cf59fe640 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xb8d98a102b0079b69ffbc760c8d857a31653e56e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x5d6812722c3693078e4a0dbe3e9affc27a0b2768 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x255f1b39172f65dc6406b8bee8b08155c45fe1b6 - 2025-10-17T22:04:33.647Z + 2025-03-20T21:28:30.528Z 0.8 - https://app.uniswap.org/explore/tokens/base/0xc2fe011c3885277c7f0e7ffd45ff90cadc8ecd12 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xc1ffaef4e7d553bbaf13926e258a1a555a363a07 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x4e73420dcc85702ea134d91a262c8ffc0a72aa70 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xecaf81eb42cd30014eb44130b89bcd6d4ad98b92 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x4eae52907dba9c370e9ee99f0ce810602a4f2c63 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x25d887ce7a35172c62febfd67a1856f20faebb00 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x382ea807a61a418479318efd96f1efbc5c1f2c21 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x6468e79a80c0eab0f9a2b574c8d5bc374af59414 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x3106a0a076bedae847652f42ef07fd58589e001f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xd015422879a1308ba557510345e944b912b9ab73 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x5de8ab7e27f6e7a1fff3e5b337584aa43961beef - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xcf078da6e85389de507ceede0e3d217e457b9d49 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x1bbf25e71ec48b84d773809b4ba55b6f4be946fb - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x7039cd6d7966672f194e8139074c3d5c4e6dcf65 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x943af17c37207c9d7a27d12cb5055542a0b7afa8 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/tokens/ethereum/0xbdf43ecadc5cef51b7d1772f722e40596bc1788b + 2025-03-20T21:28:30.528Z 0.8 - https://app.uniswap.org/explore/tokens/ethereum/0x6d68015171eaa7af9a5a0a103664cf1e506ff699 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/tokens/unichain/NATIVE + 2025-03-20T21:28:30.528Z 0.8 - https://app.uniswap.org/explore/tokens/ethereum/0x6942806d1b2d5886d95ce2f04314ece8eb825833 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/tokens/arbitrum/0x6985884c4392d348587b19cb9eaaf157f13271cd + 2025-03-20T21:28:30.528Z 0.8 - https://app.uniswap.org/explore/tokens/ethereum/0x949d48eca67b17269629c7194f4b727d4ef9e5d6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/tokens/base/0x9e6a46f294bb67c20f1d1e7afb0bbef614403b55 + 2025-03-20T21:28:30.528Z 0.8 - https://app.uniswap.org/explore/tokens/ethereum/0x9361adf2b72f413d96f81ff40d794b47ce13b331 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/tokens/ethereum/0x6c3ea9036406852006290770bedfcaba0e23a0e8 + 2025-03-20T21:28:30.528Z 0.8 - https://app.uniswap.org/explore/tokens/ethereum/0x3bb1be077f3f96722ae92ec985ab37fd0a0c4c51 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/tokens/ethereum/0x467bccd9d29f223bce8043b84e8c8b282827790f + 2025-03-20T21:28:30.528Z 0.8 - https://app.uniswap.org/explore/tokens/ethereum/0xdbb7a34bf10169d6d2d0d02a6cbb436cf4381bfa - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/tokens/base/0x3ec2156d4c0a9cbdab4a016633b7bcf6a8d68ea2 + 2025-03-20T21:28:30.528Z 0.8 - https://app.uniswap.org/explore/tokens/ethereum/0x66bff695f3b16a824869a8018a3a6e3685241269 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/tokens/base/0xc0634090f2fe6c6d75e61be2b949464abb498973 + 2025-03-20T21:28:30.528Z 0.8 - https://app.uniswap.org/explore/tokens/ethereum/0x85d19fb57ca7da715695fcf347ca2169144523a7 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/tokens/arbitrum/0xe80772eaf6e2e18b651f160bc9158b2a5cafca65 + 2025-03-20T21:28:30.528Z 0.8 - https://app.uniswap.org/explore/tokens/ethereum/0x069d89974f4edabde69450f9cf5cf7d8cbd2568d - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/tokens/ethereum/0x35d8949372d46b7a3d5a56006ae77b215fc69bc0 + 2025-03-20T21:28:30.528Z 0.8 - https://app.uniswap.org/explore/tokens/ethereum/0x0fe13ffe64b28a172c58505e24c0c111d149bd47 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/tokens/ethereum/0x1abaea1f7c830bd89acc67ec4af516284b1bc33c + 2025-03-20T21:28:30.528Z 0.8 - https://app.uniswap.org/explore/tokens/ethereum/0x111111111117dc0aa78b770fa6a738034120c302 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/tokens/base/0xfde4c96c8593536e31f229ea8f37b2ada2699bb2 + 2025-03-20T21:28:30.528Z 0.8 - https://app.uniswap.org/explore/tokens/ethereum/0xdc7ac5d5d4a9c3b5d8f3183058a92776dc12f4f3 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/tokens/ethereum/0x58d97b57bb95320f9a05dc918aef65434969c2b2 + 2025-03-20T21:28:30.528Z 0.8 - https://app.uniswap.org/explore/tokens/ethereum/0x482702745260ffd69fc19943f70cffe2cacd70e9 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/tokens/base/0xb33ff54b9f7242ef1593d2c9bcd8f9df46c77935 + 2025-03-20T21:28:30.528Z 0.8 - https://app.uniswap.org/explore/tokens/ethereum/0xc555d625828c4527d477e595ff1dd5801b4a600e - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/tokens/ethereum/0xbad6c59d72d44512616f25b3d160c79db5a69ddf + 2025-03-20T21:28:30.528Z 0.8 - https://app.uniswap.org/explore/tokens/ethereum/0x9eec1a4814323a7396c938bc86aec46b97f1bd82 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/tokens/base/0x1bc0c42215582d5a085795f4badbac3ff36d1bcb + 2025-03-20T21:28:30.528Z 0.8 - https://app.uniswap.org/explore/tokens/ethereum/0x87d73e916d7057945c9bcd8cdd94e42a6f47f776 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/tokens/ethereum/0x31c8eacbffdd875c74b94b077895bd78cf1e64a3 + 2025-03-20T21:28:30.528Z 0.8 - https://app.uniswap.org/explore/tokens/ethereum/0x067def80d66fb69c276e53b641f37ff7525162f6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/tokens/ethereum/0x667102bd3413bfeaa3dffb48fa8288819e480a88 + 2025-03-20T21:28:30.528Z 0.8 - https://app.uniswap.org/explore/tokens/ethereum/0xdd157bd06c1840fa886da18a138c983a7d74c1d7 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/tokens/polygon/0xdf7837de1f2fa4631d716cf2502f8b230f1dcc32 + 2025-03-20T21:28:30.528Z 0.8 - https://app.uniswap.org/explore/tokens/arbitrum/0xe80772eaf6e2e18b651f160bc9158b2a5cafca65 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/tokens/ethereum/0x45804880de22913dafe09f4980848ece6ecbaf78 + 2025-03-20T21:28:30.528Z 0.8 - https://app.uniswap.org/explore/tokens/arbitrum/0xb6093b61544572ab42a0e43af08abafd41bf25a6 - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/tokens/ethereum/0xd9fcd98c322942075a5c3860693e9f4f03aae07b + 2025-03-20T21:28:30.528Z 0.8 - https://app.uniswap.org/explore/tokens/arbitrum/0x35ca1e5a9b1c09fa542fa18d1ba4d61c8edff852 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x83e60b9f7f4db5cdb0877659b1740e73c662c55b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x4d01397994aa636bdcc65c9e8024bc497498c3bb - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0xc3abc47863524ced8daf3ef98d74dd881e131c38 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x4d15a3a2286d883af0aa1b3f21367843fac63e07 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0xfb7f8a2c0526d01bfb00192781b7a7761841b16c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x3809dcdd5dde24b37abe64a5a339784c3323c44f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x85955046df4668e1dd369d2de9f3aeb98dd2a369 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x554cd6bdd03214b10aafa3e0d4d42de0c5d2937b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x4318cb63a2b8edf2de971e2f17f77097e499459d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0xab9cb20a28f97e189ca0b666b8087803ad636b3c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x6a8ec2d9bfbdd20a7f5a4e89d640f7e7ceba4499 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x385eeac5cb85a38a9a07a70c73e0a3271cfb54a7 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x0169ec1f8f639b32eec6d923e24c2a2ff45b9dd6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xe161be4a74ab8fa8706a2d03e67c02318d0a0ad6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x4d58608eff50b691a3b76189af2a7a123df1e9ba - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x420b0fa3de2efcf2b2fd04152eb1df36a09717cd - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x1cd38856ee0fdfd65c757e530e3b1de3061008d3 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xfad8cb754230dbfd249db0e8eccb5142dd675a0d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xda761a290e01c69325d12d82ac402e5a73d62e81 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xafb5d4d474693e68df500c9c682e6a2841f9661a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xfc5462143a3178cf044e97c491f6bcb5e38f173e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xed1978d01d4a8a9d6a43ac79403d5b8dfbed739b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xba71cb8ef2d59de7399745793657838829e0b147 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x10c1b6f768e13c624a4a23337f1a5ba5c9be0e4b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x1b1514c76c54ce8807d7fdedf85c664eee734ece - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x58cd93c4a91c3940109fa27d700f5013b18b5dc2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xea6f7e7e0f46a9e0f4e2048eb129d879f609d632 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x30d19fb77c3ee5cfa97f73d72c6a1e509fa06aef - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xe2dca969624795985f2f083bcd0b674337ba130a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xbb7d61d2511fd2e63f02178ca9b663458af9fc63 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x59f4f336bf3d0c49dbfba4a74ebd2a6ace40539a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x62d0a8458ed7719fdaf978fe5929c6d342b0bfce - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xb8fda5aee55120247f16225feff266dfdb381d4c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xca530408c3e552b020a2300debc7bd18820fb42f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x3ffeea07a27fab7ad1df5297fa75e77a43cb5790 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xcfeb09c3c5f0f78ad72166d55f9e6e9a60e96eec - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x467bccd9d29f223bce8043b84e8c8b282827790f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x2077d81d0c5258230d5a195233941547cb5f0989 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xa0bbbe391b0d0957f1d013381b643041d2ca4022 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xd1b89856d82f978d049116eba8b7f9df2f342ff3 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x62f03b52c377fea3eb71d451a95ad86c818755d1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x3927fb89f34bbee63351a6340558eebf51a19fb8 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xacd2c239012d17beb128b0944d49015104113650 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x86b69f38bea3e02f68ff88534bc61ec60e772b19 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x6873c95307e13beb58fb8fcddf9a99667655c9e4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x18084fba666a33d37592fa2633fd49a74dd93a88 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x6e79b51959cf968d87826592f46f819f92466615 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x80ee5c641a8ffc607545219a3856562f56427fe9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x0414d8c87b271266a5864329fb4932bbe19c0c49 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xf57e7e7c23978c3caec3c3548e3d615c346e79ff - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0xb0ffa8000886e57f86dd5264b9582b2ad87b2b91 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x1c986661170c1834db49c3830130d4038eeeb866 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x9ed7e4b1bff939ad473da5e7a218c771d1569456 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x7f9a7db853ca816b9a138aee3380ef34c437dee0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x371c7ec6d8039ff7933a2aa28eb827ffe1f52f07 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0xb1bc21f748ae2be95674876710bc6d78235480e0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0xadf5dd3e51bf28ab4f07e684ecf5d00691818790 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/optimism/0x1eba7a6a72c894026cd654ac5cdcf83a46445b08 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x38022a157b95c52d43abcac9bd09f028a1079105 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0xd2507e7b5794179380673870d88b22f94da6abe0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0xc708d6f2153933daa50b2d0758955be0a93a8fec - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x0052074d3eb1429f39e5ea529b54a650c21f5aa4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x4e78011ce80ee02d2c3e649fb657e45898257815 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x7583feddbcefa813dc18259940f76a02710a8905 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0xe78aee6ccb05471a69677fb74da80f5d251c042b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x04f177fcacf6fb4d2f95d41d7d3fee8e565ca1d0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0xa6da8c8999c094432c77e7d318951d34019af24b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x6d3b8c76c5396642960243febf736c6be8b60562 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x7cf7132ede0ca592a236b6198a681bb7b42dd5ae - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x3afeae00a594fbf2e4049f924e3c6ac93296b6e8 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x0a93a7be7e7e426fc046e204c44d6b03a302b631 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xc9b6ef062fab19d3f1eabc36b1f2e852af1acd18 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x1754e5aadce9567a95f545b146a616ce34eead53 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xdb173587d459ddb1b9b0f2d6d88febef039304a2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x10a7a84c91988138f8dbbc82a23b02c8639e2552 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x92af6f53febd6b4c6f5293840b6076a1b82c4bc2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xeb9e49fb4c33d9f6aefb1b03f9133435e24c0ec6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x1b2c141479757b8643a519be4692904088d860b2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x4d25e94291fe8dcfbfa572cbb2aaa7b755087c91 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x8e0e798966382e53bfb145d474254cbe065c17dc - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x4b6f82a4ed0b9e3767f53309b87819a78d041a7f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x004aa1586011f3454f487eac8d0d5c647d646c69 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x741777f6b6d8145041f73a0bddd35ae81f55a40f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xc6c58f600917de512cd02d2b6ed595ab54b4c30f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x03aa6298f1370642642415edc0db8b957783e8d6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x3ee2200efb3400fabb9aacf31297cbdd1d435d47 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x0d8ce2a99bb6e3b7db580ed848240e4a0f9ae153 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xa697e272a73744b343528c3bc4702f2565b2f422 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x301af3eff0c904dc5ddd06faa808f653474f7fcc - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x776f9987d9deed90eed791cbd824d971fd5ccf09 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xf7de7e8a6bd59ed41a4b5fe50278b3b7f31384df - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x19e6bfc1a6e4b042fb20531244d47e252445df01 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x4338665cbb7b2485a8855a139b75d5e34ab0db94 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x2940566eb50f15129238f4dc599adc4f742d7d8e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xbb73bb2505ac4643d5c0a99c2a1f34b3dfd09d11 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x4ea98c1999575aaadfb38237dd015c5e773f75a2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/celo/0x1d18d0386f51ab03e7e84e71bda1681eba865f1f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x57b96d4af698605563a4653d882635da59bf11af - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xd33526068d116ce69f19a9ee46f0bd304f21a51f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x2a5fa016ffb20c70e2ef36058c08547f344677aa - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xbe0ed4138121ecfc5c0e56b40517da27e6c5226b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x9fd9278f04f01c6a39a9d1c1cd79f7782c6ade08 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x054c9d4c6f4ea4e14391addd1812106c97d05690 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x7613c48e0cd50e42dd9bf0f6c235063145f6f8dc - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x614da3b37b6f66f7ce69b4bbbcf9a55ce6168707 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x069e4aa272d17d9625aa3b6f863c7ef6cfb96713 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x24da31e7bb182cb2cabfef1d88db19c2ae1f5572 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x7d4a23832fad83258b32ce4fd3109ceef4332af4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xb58e61c3098d85632df34eecfb899a1ed80921cb - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x67c4d14861f9c975d004cfb3ac305bee673e996e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x69babe9811cc86dcfc3b8f9a14de6470dd18eda4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x32f0d04b48427a14fb3cbc73db869e691a9fec6f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x4cff49d0a19ed6ff845a9122fa912abcfb1f68a6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x51cb253744189f11241becb29bedd3f1b5384fdb - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xcf4c91ecafc43c9f382db723ba20b82efa852821 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x6968676661ac9851c38907bdfcc22d5dd77b564d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x0d438f3b5175bebc262bf23753c1e53d03432bde - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xb98d4c97425d9908e66e53a6fdf673acca0be986 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x68a47fe1cf42eba4a030a10cd4d6a1031ca3ca0a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x8a370c951f34e295b2655b47bb0985dd08d8f718 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x525574c899a7c877a11865339e57376092168258 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xd9a442856c234a39a81a089c06451ebaa4306a72 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x1c43d05be7e5b54d506e3ddb6f0305e8a66cd04e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0xb766039cc6db368759c1e56b79affe831d0cc507 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x18c14c2d707b2212e17d1579789fc06010cfca23 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0xe0ee18eacafddaeb38f8907c74347c44385578ab - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x56659245931cb6920e39c189d2a0e7dd0da2d57b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0xb6a5ae40e79891e4deadad06c8a7ca47396df21c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x04565fe9aa3ae571ada8e1bebf8282c4e5247b2a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xf8a99f2bf2ce5bb6ce4aafcf070d8723bc904aa2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x3b9728bd65ca2c11a817ce39a6e91808cceef6fd - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x6797b6244fa75f2e78cdffc3a4eb169332b730cc - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xe2c86869216ac578bd62a4b8313770d9ee359a05 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x47b464edb8dc9bc67b5cd4c9310bb87b773845bd - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x28a730de97dc62a8c88363e0b1049056f1274a70 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xba5ede8d98ab88cea9f0d69918dde28dc23c2553 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x8319767a7b602f88e376368dca1b92d38869b9b4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x461ee40928677644b8195662ab91bcdaae6ef105 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x24569d33653c404f90af10a2b98d6e0030d3d267 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x22222bd682745cf032006394750739684e45a5f8 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x9124577428c5bd73ad7636cbc5014081384f29d6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xaa6cccdce193698d33deb9ffd4be74eaa74c4898 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xe095780ba2a64a4efa7a74830f0b71656f0b0ad4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xb59c8912c83157a955f9d715e556257f432c35d7 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x7771450ece9c61430953d2646f995e33a06c91f5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xc48823ec67720a04a9dfd8c7d109b2c3d6622094 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x9ec02756a559700d8d9e79ece56809f7bcc5dc27 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x3593d125a4f7849a1b059e64f4517a86dd60c95d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xb0ffa8000886e57f86dd5264b9582b2ad87b2b91 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x6985884c4392d348587b19cb9eaaf157f13271cd - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xa045fe936e26e1e1e1fb27c1f2ae3643acde0171 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xbeef698bd78139829e540622d5863e723e8715f1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x426a688ee72811773eb64f5717a32981b56f10c1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x873259322be8e50d80a4b868d186cc5ab148543a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x661c70333aa1850ccdbae82776bb436a0fcfeefb - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x0a2c375553e6965b42c135bb8b15a8914b08de0c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x6fba952443be1de22232c824eb8d976b426b3c38 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x1abaea1f7c830bd89acc67ec4af516284b1bc33c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xb62132e35a6c13ee1ee0f84dc5d40bad8d815206 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xb60fdf036f2ad584f79525b5da76c5c531283a1b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x5a3e6a77ba2f983ec0d371ea3b475f8bc0811ad5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x55296f69f40ea6d20e478533c15a6b08b654e758 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x1a7e4e63778b4f12a199c062f3efdd288afcbce8 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x45804880de22913dafe09f4980848ece6ecbaf78 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xe5018913f2fdf33971864804ddb5fca25c539032 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x6985884c4392d348587b19cb9eaaf157f13271cd - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x2c650dab03a59332e2e0c0c4a7f726913e5028c1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x9aee3c99934c88832399d6c6e08ad802112ebeab - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x439c0cf1038f8002a4cad489b427e217ba4b42ad - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/optimism/0x6985884c4392d348587b19cb9eaaf157f13271cd - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x6985884c4392d348587b19cb9eaaf157f13271cd - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x6985884c4392d348587b19cb9eaaf157f13271cd - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xb79dd08ea68a908a97220c76d19a6aa9cbde4376 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x4b61e2f1bbdee6d746209a693156952936f1702c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x7480527815ccae421400da01e052b120cc4255e9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x7466de7bb8b5e41ee572f4167de6be782a7fa75d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x298d411511a05dc1b559ed8f79c56bee06687b14 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x8e16d46cb2da01cdd49601ec73d7b0344969ae33 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x18dd5b087bca9920562aff7a0199b96b9230438b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x37f0c2915cecc7e977183b8543fc0864d03e064c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x37f24b26bcefbfac7f261b97f8036da98f81a299 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xacb5b33ce55ba7729e38b2b59677e71c0112f0d9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x6985884c4392d348587b19cb9eaaf157f13271cd - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xc71b5f631354be6853efe9c3ab6b9590f8302e81 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x7e744bbb1a49a44dfcc795014a4ba618e418fbbe - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x4e3fbd56cd56c3e72c1403e103b45db9da5b9d2b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x0c04ff41b11065eed8c9eda4d461ba6611591395 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x636bd98fc13908e475f56d8a38a6e03616ec5563 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x590246bfbf89b113d8ac36faeea12b7589f7fe5b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x80034f803afb1c6864e3ca481ef1362c54d094b9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x73fbd93bfda83b111ddc092aa3a4ca77fd30d380 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xff33a6b3dc0127862eedd3978609404b22298a54 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xc770eefad204b5180df6a14ee197d99d808ee52d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xa0385e7283c83e2871e9af49eec0966088421ddd - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xb2617246d0c6c0087f18703d576831899ca94f01 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xba386a4ca26b85fd057ab1ef86e3dc7bdeb5ce70 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x9ebb0895bd9c7c9dfab0d8d877c66ba613ac98ea - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xd12a99dbc40036cec6f1b776dccd2d36f5953b94 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x8ab2ff0116a279a99950c66a12298962d152b83c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x420698cfdeddea6bc78d59bc17798113ad278f9d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xa8c8cfb141a3bb59fea1e2ea6b79b5ecbcd7b6ca - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xd8e8438cf7beed13cfabc82f300fb6573962c9e3 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xb1c9d42fa4ba691efe21656a7e6953d999b990c4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0xdadeca1167fe47499e53eb50f261103630974905 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0xa05245ade25cc1063ee50cf7c083b4524c1c4302 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x4fafad147c8cd0e52f83830484d164e960bdc6c3 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x4dd9077269dd08899f2a9e73507125962b5bc87f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x8931ee05ec111325c1700b68e5ef7b887e00661d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x26f1bb40ea88b46ceb21557dc0ffac7b7c0ad40f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x642e993fa91ffe9fb24d39a8eb0e0663145f8e92 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x0c41f1fc9022feb69af6dc666abfe73c9ffda7ce - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xf7ccb8a6e3400eb8eb0c47619134f7516e025215 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x2416092f143378750bb29b79ed961ab195cceea5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xf0268c5f9aa95baf5c25d646aabb900ac12f0800 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x0c067fc190cde145b0c537765a78d4e19873a5cc - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xbe5614875952b1683cb0a2c20e6509be46d353a4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x87a0233a8cb4392ec3eb8fa467817fc0b6a326dd - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xdfbea88c4842d30c26669602888d746d30f9d60d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xb6fe221fe9eef5aba221c348ba20a1bf5e73624c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x80b3455e1db60b4cba46aba12e8b1e256dd64979 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x747747e47a48c669be384e0dfb248eee6ba04039 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/celo/0x50e85c754929840b58614f48e29c64bc78c58345 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x02f92800f57bcd74066f5709f1daa1a4302df875 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x967da4048cd07ab37855c090aaf366e4ce1b9f48 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x729031b3995538ddf6b6bce6e68d5d6fdeb3ccb5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x6dea81c8171d0ba574754ef6f8b412f2ed88c54d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x97a9a15168c22b3c137e6381037e1499c8ad0978 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x5faa989af96af85384b8a938c2ede4a7378d9875 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x4691937a7508860f876c9c0a2a617e7d9e945d4b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xb50721bcf8d664c30412cfbc6cf7a15145234ad1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x037a54aab062628c9bbae1fdb1583c195585fe41 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0xcb8b5cd20bdcaea9a010ac1f8d835824f5c87a04 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0xdfb8be6f8c87f74295a87de951974362cedcfa30 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x354a6da3fcde098f8389cad84b0182725c6c91de - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x3f56e0c36d275367b8c502090edf38289b3dea0d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x6f9590958ce2beaf9c92a3a8fca6d1ddf310e052 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/optimism/0x3e5d9d8a63cc8a88748f229999cf59487e90721e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/optimism/0xecc68d0451e20292406967fe7c04280e5238ac7d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0xf1c1a3c2481a3a8a3f173a9ab5ade275292a6fa3 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0xb5e0cfe1b4db501ac003b740665bf43192cc7853 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0xffa188493c15dfaf2c206c97d8633377847b6a52 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0xb5c064f955d8e7f38fe0460c556a72987494ee17 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x4f604735c1cf31399c6e711d5962b2b3e0225ad3 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0xf0949dd87d2531d665010d6274f06a357669457a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x14e5386f47466a463f85d151653e1736c0c50fc3 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0xadac33f543267c4d59a8c299cf804c303bc3e4ac - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xcfa3ef56d303ae4faaba0592388f19d7c3399fb4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x67ce18961c3269ca03c2e5632f1938cc53e614a1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x48164ea5df090e80a0eaee1147e466ea28669221 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x3054e8f8fba3055a42e5f5228a2a4e2ab1326933 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x42069d11a2cc72388a2e06210921e839cfbd3280 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x74ff3cbf86f95fea386f79633d7bc4460d415f34 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x2d6a3893966dda77749cc7e4003ab15f5cfa3cc1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x51b75da3da2e413ea1b8ed3eb078dc712304761c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x8ad5b9007556749de59e088c88801a3aaa87134b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xbd97693278f1948c59f65f130fd87e7ff7c61d11 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x3992b27da26848c2b19cea6fd25ad5568b68ab98 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x34980c35353a8d7b1a1ba02e02e387a8383e004a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xdebd6e2da378784a69dc6ec99fe254223b312287 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/celo/0x456a3d042c0dbd3db53d5489e98dfb038553b0d0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/celo/0x9995cc8f20db5896943afc8ee0ba463259c931ed - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x30d20208d987713f46dfd34ef128bb16c404d10f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x19848077f45356b21164c412eff3d3e4ff6ebc31 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x53206bf5b6b8872c1bb0b3c533e06fde2f7e22e4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x07ddacf367f0d40bd68b4b80b4709a37bdc9f847 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xbdbe9f26918918bd3f43a0219d54e5fda9ce1bb3 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xb9d09bc374577dac1ab853de412a903408204ea8 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xe72b141df173b999ae7c1adcbf60cc9833ce56a8 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x214549b0317564de15770561221433fb3e8c995c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xc82e3db60a52cf7529253b4ec688f631aad9e7c2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xf3dcbc6d72a4e1892f7917b7c43b74131df8480e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x62e3b3c557c792c4a70765b3cdb5b56b1879f82d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x2598c30330d5771ae9f983979209486ae26de875 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xd4f4d0a10bcae123bb6655e8fe93a30d01eebd04 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0xa0995d43901551601060447f9abf93ebc277cec2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x40379a439d4f6795b6fc9aa5687db461677a2dba - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x433cde5a82b5e0658da3543b47a375dffd126eb6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x619c4bbbd65f836b78b36cbe781513861d57f39d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x1e0bb24ed6c806c01ef2f880a4b91adb90099ea7 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x0dd7913197bfb6d2b1f03f9772ced06298f1a644 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xfbb75a59193a3525a8825bebe7d4b56899e2f7e1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xc3de830ea07524a0761646a6a4e4be0e114a3c83 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x3792dbdd07e87413247df995e692806aa13d3299 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x527856315a4bcd2f428ea7fa05ea251f7e96a50a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x292fcdd1b104de5a00250febba9bc6a5092a0076 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xd749b369d361396286f8cc28a99dd3425ac05619 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xfe3e6a25e6b192a42a44ecddcd13796471735acf - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xa1faa113cbe53436df28ff0aee54275c13b40975 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x8802269d1283cdb2a5a329649e5cb4cdcee91ab6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x0000bdaa645097ef80f9d475f341d0d107a45b3a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x683a4ac99e65200921f556a19dadf4b0214b5938 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x36c7188d64c44301272db3293899507eabb8ed43 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x8a2279d4a90b6fe1c4b30fa660cc9f926797baa2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xf418588522d5dd018b425e472991e52ebbeeeeee - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x6135177a17e02658df99a07a2841464deb5b8589 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xcf91b70017eabde82c9671e30e5502d312ea6eb2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x45080a6531d671ddff20db42f93792a489685e32 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x790814cd782983fab4d7b92cf155187a865d9f18 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x9e6be44cc1236eef7e1f197418592d363bedcd5a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xaa7a9ca87d3694b5755f213b5d04094b8d0f0a6f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x69ee720c120ec7c9c52a625c04414459b3185f23 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x408e41876cccdc0f92210600ef50372656052a38 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x5cf04716ba20127f1e2297addcf4b5035000c9eb - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x8290333cef9e6d528dd5618fb97a76f268f3edd4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x1929761e87667283f087ea9ab8370c174681b4e9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x888888848b652b3e3a0f34c96e00eec0f3a23f72 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xf944e35f95e819e752f3ccb5faf40957d311e8c5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x1f70300bce8c2302780bd0a153ebb75b8ca7efcb - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x3de81ce90f5a27c5e6a5adb04b54aba488a6d14e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0xc87b37a581ec3257b734886d9d3a581f5a9d056c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x1a6b3a62391eccaaa992ade44cd4afe6bec8cff1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x65c936f008bc34fe819bce9fa5afd9dc2d49977f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x07d65c18cecba423298c0aeb5d2beded4dfd5736 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x51fc0f6660482ea73330e414efd7808811a57fa2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0xcbe94d75ec713b7ead84f55620dc3174beeb1cfe - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0xd3144ff5f388d36c0a445686c08540296d8b209b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x433e39ce74aef8f409182541269e417ad9b56011 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xb1a03eda10342529bbf8eb700a06c60441fef25d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x6b9bb36519538e0c073894e964e90172e1c0b41f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x689644b86075ed61c647596862c7403e1c474dbf - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x9a6d24c02ec35ad970287ee8296d4d6552a31dbe - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x506beb7965fc7053059006c7ab4c62c02c2d989f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x31b28012f61fc3600e1c076bafc9fd997fb2da90 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xd7d919ea0c33a97ad6e7bd4f510498e2ec98cb78 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xef553b6914dbd17567393f7e55fbd773fff7d0cb - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xe642657e4f43e6dcf0bd73ef24008394574dee28 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xf8b1b47aa748f5c7b5d0e80c726a843913eb573a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xd064c53f043d5aee2ac9503b13ee012bf2def1d0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xfc60aa1ffca50ce08b3cdec9626c0bb9e9b09bec - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x82c8f48ac694841360de84d649a0d48d239b61f8 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x7d89e05c0b93b24b5cb23a073e60d008fed1acf9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x7546e0d4d947a15f914e33de6616ffed826f45ef - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x9a5350edf28c1f93bb36d6e94b5c425fde8e222d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xaa076b62efc6f357882e07665157a271ab46a063 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x6a6aa13393b7d1100c00a57c76c39e8b6c835041 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x07040971246a73ebda9cf29ea1306bb47c7c4e76 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x6df0e641fc9847c0c6fde39be6253045440c14d3 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x2b640a99991dea2916205ecdc9f9c58f80017ed8 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x38e4adb44ef08f22f5b5b76a8f0c2d0dcbe7dca1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x42069cc15f5befb510430d22ff1c9a1b3ae22cfe - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x40d16fc0246ad3160ccc09b8d0d3a2cd28ae6c2f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x89fd2d8fd8d937f55c89b7da3ceed44fa27e4a81 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x76bc677d444f1e9d57daf5187ee2b7dc852745ae - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xa0084063ea01d5f09e56ef3ff6232a9e18b0bacd - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x4abd5745f326932b1b673bfa592a20d7bb6bc455 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xe53ec727dbdeb9e2d5456c3be40cff031ab40a55 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xf43f21384d03b5cbbddd58d2de64071e4ce76ab0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x33349b282065b0284d756f0577fb39c158f935e6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x33c88d4cac6ac34f77020915a2a88cd0417dc069 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0xdce765f021410b3266aa0053c93cb4535f1e12e0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0xb50a8e92cb9782c9b8f3c88e4ee8a1d0aa2221d7 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x0a84edf70f30325151631ce7a61307d1f4d619a3 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0xc11158c5da9db1d553ed28f0c2ba1cbedd42cfcb - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/optimism/0xb0b195aefa3650a6908f15cdac7d92f8a5791b0b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0xdc4f4ed9872571d5ec8986a502a0d88f3a175f1e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x9beec80e62aa257ced8b0edd8692f79ee8783777 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xf95e1c0a67492720ca22842122fe7fa63d5519e5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xca8e8d244f0d219a6fc9e4793c635cea98d0399c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x6a4f69da1e2fb2a9b11d1aad60d03163fe567732 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x0718f45bbf4781ce891e4e18182f025725f0fc95 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x132bbda4a40d4d6288be49b637ec2c113b5d7600 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x9aaae745cf2830fb8ddc6248b17436dc3a5e701c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x24fcfc492c1393274b6bcd568ac9e225bec93584 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x21fd16cd0ef24a49d28429921e335bb0c1bfadb3 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xa469b7ee9ee773642b3e93e842e5d9b5baa10067 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x8c19f7854b27758ddffdcdc8908f22bf55e00736 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0xf2ae0038696774d65e67892c9d301c5f2cbbda58 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x6bc40d4099f9057b23af309c08d935b890d7adc0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xee2a03aa6dacf51c18679c516ad5283d8e7c2637 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x7f911119435d8ded9f018194b4b6661331379a3d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x777be1c6075c20184c4fd76344b7b0b7c858fe6b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x812ba41e071c7b7fa4ebcfb62df5f45f6fa853ee - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x881d4c8618d68872fa404518b2460ea839a02a6a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xba2ae4e0a9c6ecaf172015aa2cdd70a21f5a290b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x1caf237d7a2d103e3e9b1855988c01ac10344600 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x7d4a7be025652995364e0e232063abd9e8d65e6e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x620aa20875ec1144126ea47fb27ecfe6e10d0c56 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xfae103dc9cf190ed75350761e95403b7b8afa6c0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xae7ab96520de3a18e5e111b5eaab095312d7fe84 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x04c154b66cb340f3ae24111cc767e0184ed00cc6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x70e8de73ce538da2beed35d14187f6959a8eca96 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xfb7b4564402e5500db5bb6d63ae671302777c75a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x6810e776880c02933d47db1b9fc05908e5386b96 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x11e969e9b3f89cb16d686a03cd8508c9fc0361af - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x8b5d1d8b3466ec21f8ee33ce63f319642c026142 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x3ed03e95dd894235090b3d4a49e0c3239edce59e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0xb3f13b0c61d65d67d7d6215d70c89533ee567a91 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0xfea31d704deb0975da8e77bf13e04239e70d7c28 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/optimism/0x66e535e8d2ebf13f49f3d49e5c50395a97c137b1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x9a06db14d639796b25a6cec6a1bf614fd98815ec - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x7fdd7419428955dbf36d4176af5a8f09ad29d1f3 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x8c9037d1ef5c6d1f6816278c7aaf5491d24cd527 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xa9f5031b54c44c3603b4300fde9b8f5cd18ad06f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x57f5fbd3de65dfc0bd3630f732969e5fb97e6d37 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x9ef1139e6b420cc929dd912a5a7adeced6f12e91 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x120edc8e391ba4c94cb98bb65d8856ae6ec1525f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xd7ea82d19f1f59ff1ae95f1945ee6e6d86a25b96 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x2c9ab600d71967ff259c491ad51f517886740cbc - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xf4c8e32eadec4bfe97e0f595add0f4450a863a11 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x8c49a510756224e887b3d99d00d959f2d86dda1c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x7777cec341e7434126864195adef9b05dcc3489c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x19af07b52e5faa0c2b1e11721c52aa23172fe2f5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xb7109df1a93f8fe2b8162c6207c9b846c1c68090 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xbbc2ae13b23d715c30720f079fcd9b4a74093505 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x595832f8fc6bf59c85c527fec3740a1b7a361269 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x7316d973b0269863bbfed87302e11334e25ea565 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xa0b73e1ff0b80914ab6fe0444e65848c4c34450b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x2be8e422cb4a5a7f217a8f1b0658952a79132f28 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x83e6f1e41cdd28eaceb20cb649155049fac3d5aa - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xbabe3ce7835665464228df00b03246115c30730a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x2e6a60492fb5b58f5b5d08c7cafc75e740e6dc8e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xc08e7e23c235073c6807c2efe7021304cb7c2815 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x955d5c14c8d4944da1ea7836bd44d54a8ec35ba1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x3540abe4f288b280a0740ad5121aec337c404d15 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xfe8526a77a2c3590e5973ba81308b90bea21fbff - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x64aa3364f17a4d01c6f1751fd97c2bd3d7e7f1d5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xd807f7e2818db8eda0d28b5be74866338eaedb86 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x4186bfc76e2e237523cbc30fd220fe055156b41f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0xd5d3aa404d7562d09a848f96a8a8d5d65977bf90 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0xa3f751662e282e83ec3cbc387d225ca56dd63d3a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0xd24157aa1097486dc9d7cf094a7e15026e566b5d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0xbed0b9240bdbcc8e33f66d2ca650a5ef60a5bab0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x5d559ea7bb2dae4b694a079cb8328a2145fd32f6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x97b959385dfdcaf252223838746beb232ac601aa - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x18e692c03de43972fe81058f322fa542ae1a5e2c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x38029c62dfa30d9fd3cadf4c64e9b2ab21dbda17 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x4507cef57c46789ef8d1a19ea45f4216bae2b528 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/celo/0x73f93dcc49cb8a239e2032663e9475dd5ef29a08 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x9e523234d36973f9e38642886197d023c88e307e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x5de758bba013e58dae2693aea3f0b12b31a3023d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x1001271083c249bd771e1bb76c22d935809a61ee - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x9d39a5de30e57443bff2a8307a4256c8797a3497 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xf3768d6e78e65fc64b8f12ffc824452130bd5394 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xf2ec4a773ef90c58d98ea734c0ebdb538519b988 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x0f2d719407fdbeff09d87557abb7232601fd9f29 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x180000dda70eb7fb7f3e10e52e88ce88f46e3b3a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xed89fc0f41d8be2c98b13b7e3cd3e876d73f1d30 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x17c50d62e6e8d20d2dc18e9ad79c43263d0720d9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x3b50805453023a91a8bf641e279401a0b23fa6f9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xfd03723a9a3abe0562451496a9a394d2c4bad4ab - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xfe67a4450907459c3e1fff623aa927dd4e28c67a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xc5fb36dd2fb59d3b98deff88425a3f425ee469ed - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x6b021b3f68491974be6d4009fee61a4e3c708fd6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x7ae9ab13fc8945323b778b3f8678145e80ec2efb - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0xbc4c97fb9befaa8b41448e1dfcc5236da543217f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/optimism/0x93919784c523f39cacaa98ee0a9d96c3f32b593e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0xd55fce7cdab84d84f2ef3f99816d765a2a94a509 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x32e0f9d26d1e33625742a52620cc76c1130efde6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x9b700b043e9587dde9a0c29a9483e2f8fa450d54 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x0b1594b0e896bf165d925956e0df733b8443af6a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x891502ba08132653151f822a3a430198f1844115 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xc702b80a1bebac118cab22ce6f2978ef59563b3f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x1287a235474e0331c0975e373bdd066444d1bd35 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xab36452dbac151be02b16ca17d8919826072f64a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xcc7ff230365bd730ee4b352cc2492cedac49383e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xa9b038285f43cd6fe9e16b4c80b4b9bccd3c161b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x77be1ba1cd2d7a63bffc772d361168cc327dd8bc - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x00000000efe302beaa2b3e6e1b18d08d69a9012a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xd101dcc414f310268c37eeb4cd376ccfa507f571 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xd09eb9099fac55edcbf4965e0a866779ca365a0c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x7b0df1cd724ec34ec9bc4bd19749b01afb490761 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x71297312753ea7a2570a5a3278ed70d9a75f4f44 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x9e32b13ce7f2e80a01932b42553652e053d6ed8e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x6942040b6d25d6207e98f8e26c6101755d67ac89 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x3301ee63fb29f863f2333bd4466acb46cd8323e6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xfefe157c9d0ae025213092ff9a5cb56ab492bab8 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x44108f0223a3c3028f5fe7aec7f9bb2e66bef82f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x1121acc14c63f3c872bfca497d10926a6098aac5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xf1376bcef0f78459c0ed0ba5ddce976f1ddf51f4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xce722f60f35c37ab295adc4e6ba45bcc7ca89dd6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x614577036f0a024dbc1c88ba616b394dd65d105a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x93fa0b88c0c78e45980fa74cdd87469311b7b3e4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0xe22c452bd2ade15dfc8ad98286bc6bdf0c9219b7 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x00000000000451f49c692bfc24971cacea2db678 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x00000000702749f73e5210b08b0a3d440078f888 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x86f65121804d2cdbef79f9f072d4e0c2eebabc08 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x127e47aba094a9a87d084a3a93732909ff031419 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x52b492a33e447cdb854c7fc19f1e57e8bfa1777d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x55027a5b06f4340cc4c82dcc74c90ca93dcb173e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x32b133add6d99d085ff23f522662b546b70d54a1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x2ad3d80c917ddbf08acc04277f379e00e4d75395 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xc73dc7ae7a4fa40517aafa941ae1ee436b91a12c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x9f235d23354857efe6c541db92a9ef1877689bcb - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x0c90c756350fb803a7d5d9f9ee5ac29e77369973 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xac12f930318be4f9d37f602cbf89cd33e99aa9d4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x1c45366641014069114c78962bdc371f534bc81c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xc328a59e7321747aebbc49fd28d1b32c1af8d3b2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x90edf25b14393350f0c1b5b12b6cb3cd3781fb4a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x590f820444fa3638e022776752c5eef34e2f89a6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x1fdb29ad49330b07ae5a87483f598aa6b292039e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x4a220e6096b25eadb88358cb44068a3248254675 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xabd4c63d2616a5201454168269031355f4764337 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x4c1b1302220d7de5c22b495e78b72f2dd2457d45 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x5d3a1ff2b6bab83b63cd9ad0787074081a52ef34 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x050c24dbf1eec17babe5fc585f06116a259cc77a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x57211299bc356319ba5ca36873eb06896173f8bc - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xfde4c96c8593536e31f229ea8f37b2ada2699bb2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xf9b738c2e7adc4f299c57afd0890b925a5efea6f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x04c0599ae5a44757c0af6f9ec3b93da8976c150a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x99b2b1a2adb02b38222adcd057783d7e5d1fcc7d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xf9569cfb8fd265e91aa478d86ae8c78b8af55df4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xa3d1a8deb97b111454b294e2324efad13a9d8396 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xd85eff20288ca72ea9eecffb428f89ee5066ca5c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x13f4196cc779275888440b3000ae533bbbbc3166 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x160452f95612699d1a561a70eeeeede67c6812af - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x5ce12f6d9f2fcaf0b11494a1c39e09eeb16ca7e8 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x6894cde390a3f51155ea41ed24a33a4827d3063d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x6db6fdb5182053eecec778afec95e0814172a474 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xc0cfbe1602dd586349f60e4681bf4badca584ec9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x289ff00235d2b98b0145ff5d4435d3e92f9540a6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xcb76314c2540199f4b844d4ebbc7998c604880ca - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xd7cfdb3cdc33dbeb9e9a4c95b61953cf12a008b3 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xce176825afc335d9759cb4e323ee8b31891de747 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x8f2bf2f59cdf7be4aee71500b9419623202b8636 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x744d70fdbe2ba4cf95131626614a1763df805b9e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x52e6654aee5d59e13ae30b48f8f5dbeb97f708cd - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x38f9bf9dce51833ec7f03c9dc218197999999999 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x7189fb5b6504bbff6a852b13b7b82a3c118fdc27 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/optimism/0x38f9bf9dce51833ec7f03c9dc218197999999999 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x8349314651ede274f8c5fef01aa65ff8da75e57c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x38f9bf9dce51833ec7f03c9dc218197999999999 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x1adcef5c780d8895ac77e6ee9239b4b3ecb76da2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x38f9bf9dce51833ec7f03c9dc218197999999999 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x917f39bb33b2483dd19546b1e8d2f09ce481ee44 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x60a3e35cc302bfa44cb288bc5a4f316fdb1adb42 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x8b67f2e56139ca052a7ec49cbcd1aa9c83f2752a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x029c58a909fbe3d4be85a24f414dda923a3fde0f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x655a51e6803faf50d4ace80fa501af2f29c856cf - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x9ca5dfa3b0b187d7f53f4ef83ca435a2ec2e4070 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xb68a20b9e9b06fde873897e12ab3372ce48f1a8a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x0203d275d2a65030889af45ed91d472be3948b92 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xa00453052a36d43a99ac1ca145dfe4a952ca33b8 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x8236a87084f8b84306f72007f36f2618a5634494 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xbc5ca3c518c8a2930947661237b1b562e34f22b7 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xfd0205066521550d7d7ab19da8f72bb004b4c341 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x880226cbcce551eeafd18c9a9e883c85811b82fc - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xfc21540d6b89667d167d42086e1feb04da3e9b21 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x41d06390b935356b46ad6750bda30148ad2044a4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x8149745670881d99700078ede5903a1a7bebe262 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xcf01a5c02c9b9dd5bf73a5a56bcdbc9dca483d43 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xae0fe8474cf5b1b412b3e4327a1c535ea12b77b7 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xc98d64da73a6616c42117b582e832812e7b8d57f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x70c0b83501a3989d4f8a8693581bb7010194abb5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x80122c6a83c8202ea365233363d3f4837d13e888 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x455e53cbb86018ac2b8092fdcd39d8444affc3f6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x58aea10748a00d1781d6651f9d78a414ea32ca46 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x406d59819bc2aef682f4ff2769085c98a264f97b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0xc4ce1d6f5d98d65ee25cf85e9f2e9dcfee6cb5d6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x94025780a1ab58868d9b2dbbb775f44b32e8e6e5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0xf33687811f3ad0cd6b48dd4b39f9f977bd7165a2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xa88594d404727625a9437c3f886c7643872296ae - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x7e72d6410803c40e73806f2a72e3eade5d075cc0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x31ea904a7eca45122890deb8da3473a2081bc9d1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x48c6740bcf807d6c47c864faeea15ed4da3910ab - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xc5fecc3a29fb57b5024eec8a2239d4621e111cbe - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x184cff0e719826b966025f93e05d8c8b0a79b3f9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x0c2e08e459fc43ddd1e2718c122f566473f59665 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x1a3a8cf347b2bf5890d3d6a1b981c4f4432c8661 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x8baf5d75cae25c7df6d1e0d26c52d19ee848301a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x28561b8a2360f463011c16b6cc0b0cbef8dbbcad - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x0fd10b9899882a6f2fcb5c371e17e70fdee00c38 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x7a58c0be72be218b41c608b7fe7c5bb630736c71 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xddaf27167929cd045a7d97d09a4fa1046ece3d89 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x375e104af98872e5b4fe951919e504a47db1757c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x5408d3883ec28c2de205064ae9690142b035fed2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x1bb4afbf2ce0c9ec86e6414ad4ba4d9aab1c0de4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x7391425ca7cee3ee03e09794b819291a572af83e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x38e382f74dfb84608f3c1f10187f6bef5951de93 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xbea269038eb75bdab47a9c04d0f5c572d94b93d5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xf41a7b7c79840775f70a085c1fc5a762bbc6b180 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x13654df31871b5d01e5fba8e6c21a5d0344820f5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x4d840b741bc05fde325d4ec0b4cfcd0cea237e4e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x49b1be61a8ca3f9a9f178d6550e41e00d9162159 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xf5bc3439f53a45607ccad667abc7daf5a583633f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x0a953dd9fc813fefaf6015b804c9dfa0624690c0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x44ec807ce2f4a6f2737a92e985f318d035883e47 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xfb6115445bff7b52feb98650c87f44907e58f802 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x117a123ded97cd125837d9ac19592b77d806fa88 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xd9fcd98c322942075a5c3860693e9f4f03aae07b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x240cd7b53d364a208ed41f8ced4965d11f571b7a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xb8d6196d71cdd7d90a053a7769a077772aaac464 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xcbde0453d4e7d748077c1b0ac2216c011dd2f406 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x786f112c9a6bc840cdc07cfd840105efd6ef2d4b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x0bffdd787c83235f6f0afa0faed42061a4619b7a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x1c43cd666f22878ee902769fccda61f401814efb - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x1b54a6fa1360bd71a0f28f77a1d6fba215d498c3 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xb528edbef013aff855ac3c50b381f253af13b997 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x888888ae2c4a298efd66d162ffc53b3f2a869888 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x4cd27e18757baa3a4fe7b0ab7db083002637a6c5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x240d6faf8c3b1a7394e371792a3bf9d28dd65515 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x41b1f9dcd5923c9542b6957b9b72169595acbc5c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xd1f2586790a5bd6da1e443441df53af6ec213d83 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x8de5b80a0c1b02fe4976851d030b36122dbb8624 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x391cf4b21f557c935c7f670218ef42c21bd8d686 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x8bd35250918ed056304fa8641e083be2c42308bb - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xc3960227e41c3f54e9b399ce216149dea5315c34 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x59062301fb510f4ea2417b67404cb16d31e604ba - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x75ec618a817eb0a4a7e44ac3dfc64c963daf921a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x7e7a7c916c19a45769f6bdaf91087f93c6c12f78 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x21ccbc5e7f353ec43b2f5b1fb12c3e9d89d30dca - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/optimism/0x87eee96d50fb761ad85b1c982d28a042169d61b1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x3c720206bfacb2d16fa3ac0ed87d2048dbc401fc - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x8d60fb5886497851aac8c5195006ecf07647ba0d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xcb327b99ff831bf8223cced12b1338ff3aa322ff - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xf544251d25f3d243a36b07e7e7962a678f952691 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xa7296cefae8477a81e23230ca5d3a3d6f49d3764 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x051fb509e4a775fabd257611eea1efaed8f91359 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xae2bddbcc932c2d2cf286bad0028c6f5074c77b5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x1dd2d631c92b1acdfcdd51a0f7145a50130050c4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xd3c68968137317a57a9babeacc7707ec433548b4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x7f6f6720a73c0f54f95ab343d7efeb1fa991f4f7 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xf3527ef8de265eaa3716fb312c12847bfba66cef - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x8888888888f004100c0353d657be6300587a6ccd - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xe2a59d5e33c6540e18aaa46bf98917ac3158db0d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xfa2ad87e35fc8d3c9f57d73c4667a4651ce6ad2f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xec53bf9167f50cdeb3ae105f56099aaab9061f83 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xb3912b20b3abc78c15e85e13ec0bf334fbb924f7 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x16a3543fa6b32cac3b0a755f64a729e84f89a75c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xf1c9acdc66974dfb6decb12aa385b9cd01190e38 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x0da2082905583cedfffd4847879d0f1cf3d25c36 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xb0ffa8000886e57f86dd5264b9582b2ad87b2b91 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xec9333e7dadeebf82d290d6cb12e66cc30ce46b0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x898843fb909e3562c82f2b96f4e3d0693af041df - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xaf05ce8a2cef336006e933c02fc89887f5b3c726 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x13e4b8cffe704d3de6f19e52b201d92c21ec18bd - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xaeb3607ec434454ceb308f5cd540875efb54309a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x2a3bff78b79a009976eea096a51a948a3dc00e34 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x4298e4ad48be89bf63a6fdc470a4b4fe9ce633b1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xa117000000f279d81a1d3cc75430faa017fa5a2e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x339058ca41e17b55b6dd295373c5d3cbe8000cd9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xa3d4bee77b05d4a0c943877558ce21a763c4fa29 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x362bc847a3a9637d3af6624eec853618a43ed7d2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x7a65cb87f596caf31a4932f074c59c0592be77d7 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xa21af1050f7b26e0cff45ee51548254c41ed6b5c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x284b25d8f199125da962abc9ee6e6b1b6715cae3 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x8fac8031e079f409135766c7d5de29cf22ef897c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xf280b16ef293d8e534e370794ef26bf312694126 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x69af81e73a73b40adf4f3d4223cd9b1ece623074 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x888c1a341ce9d9ae9c2d2a75a72a7f0d2551a2dc - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x465dbc39f46f9d43c581a5d90a43e4a0f2a6ff2d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x44e18207b6e98f4a786957954e462ed46b8c95be - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x70c29e99ca32592c0e88bb571b87444bb0e08e33 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x8c7ac134ed985367eadc6f727d79e8295e11435c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x6aa56e1d98b3805921c170eb4b3fe7d4fda6d89b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x81db1949d0e888557bc632f7c0f6698b1f8c9106 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x2de1218c31a04e1040fc5501b89e3a58793b3ddf - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x30ae41d5f9988d359c733232c6c693c0e645c77e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x1fc01117e196800f416a577350cb1938d10501c2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x3212dc0f8c834e4de893532d27cc9b6001684db0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/optimism/0xd0cf4de352ac8dcce00bd6b93ee73d3cb272edc3 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x75e6b648c91d222b2f6318e8ceeed4b691d5323f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x2a06a17cbc6d0032cac2c6696da90f29d39a1a29 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x6668d4a6605a27e5ee51eda040581155eddc6666 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x2dc90fa3a0f178ba4bee16cac5d6c9a5a7b4c6cb - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x9c7beba8f6ef6643abd725e45a4e8387ef260649 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x0cf8e180350253271f4b917ccfb0accc4862f262 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x42069026eac8eee0fd9b5f7adfa4f6e6d69a2b39 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x340d2bde5eb28c1eed91b2f790723e3b160613b7 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xec21890967a8ceb3e55a3f79dac4e90673ba3c2e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x6900f7b42fb4abb615c938db6a26d73a9afbed69 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x4c44a8b7823b80161eb5e6d80c014024752607f2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x103143acf2e717acf8f021823e86a1dbfe944fb5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x6969f3a3754ab674b48b7829a8572360e98132ba - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x562e362876c8aee4744fc2c6aac8394c312d215d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xd0ebfe04adb5ef449ec5874e450810501dc53ed5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x2597342ff387b63846eb456419590781c4bfcdaf - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x4e6221c07dae8d3460a46fa01779cf17fdd72ad8 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xb612bfc5ce2fb1337bd29f5af24ca85dbb181ce2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xc0e10854ab40b2e59a5519c481161a090f1162a0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xa7f4195f10f1a62b102bd683eab131d657a6c6e4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x7e7ef0ee0305c1c195fcae22fd7b207a813eef86 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0xb6212b633c941e9be168c4b9c2d9e785f1cd42fb - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/optimism/0x139052115f8b1773cf7dcba6a553f922a2e54f69 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x3f94618ad346f34f43e27f0cf46decbb0d396b1b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xf56b3b3972f2f154555a0b62ff5a22b7b2a3c90b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xc08cd26474722ce93f4d0c34d16201461c10aa8c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x080c169cd58122f8e1d36713bf8bcbca45176905 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x50da645f148798f68ef2d7db7c1cb22a6819bb2c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xea1d649ddc8e2a6e6ee40b89b2997518476cafa5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xa4080f1778e69467e905b8d6f72f6e441f9e9484 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xb60acd2057067dc9ed8c083f5aa227a244044fd6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xd0dfca0b404e866dc9a3038bd2a545c6735d9fa9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x18a8d75f70eaead79b5a55903d036ce337f623a5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xebb66a88cedd12bfe3a289df6dfee377f2963f12 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x9343e24716659a3551eb10aff9472a2dcad5db2d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xfa3e941d1f6b7b10ed84a0c211bfa8aee907965e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x85bea4ee627b795a79583fcede229e198aa57055 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x0c03ce270b4826ec62e7dd007f0b716068639f7b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x19706c142d33376240e418d6385f05691a5fa8e2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xb3e41d6e0ea14b43bc5de3c314a408af171b03dd - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x634769eb87542eaf41c0008c05d5d8f5d8bec3a5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xd3c5bdbc6de5ea3899a28f6cd419f29c09fa749f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x9dfad1b7102d46b1b197b90095b5c4e9f5845bba - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xc8f69a9b46b235de8d0b77c355fff7994f1b090f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x5200b34e6a519f289f5258de4554ebd3db12e822 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x69fd9281a920717ee54193a1c130b689ef341933 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x5d56b6581d2e7e7574adce2dc593f499a53d7505 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x168168db04def453b7e8bfaff1e0102a3e810485 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x1f19d846d99a0e75581913b64510fe0e18bbc31f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x571d9b73dc04ed88b4e273e048c8d4848f83b779 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0xca5ca9083702c56b481d1eec86f1776fdbd2e594 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x99f40b01ba9c469193b360f72740e416b17ac332 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/optimism/0xc6bdfc4f2e90196738873e824a9efa03f7c64176 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x06480acaae64bcfa6da8fd176f60982584385090 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x0c5142bc58f9a61ab8c3d2085dd2f4e550c5ce0b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xc734635cd30e882037c3f3de1ebccf9fa9d27d9f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x65e570b560027f493f2b1907e8e8e3b9546053bd - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xd1917629b3e6a72e6772aab5dbe58eb7fa3c2f33 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x9e81f6495ba29a6b4d48bddd042c0598fa8abc9f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x2075f6e2147d4ac26036c9b4084f8e28b324397d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x01aac2b594f7bdbec740f0f1aa22910ebb4b74ab - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xef433ebb8ba7a486ce21b854f093b9a3f4e696bc - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x2bb84fd8f7ed0ffae3da36ad60d4d7840bdeeada - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xad86b91a1d1db15a4cd34d0634bbd4ecacb5b61a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x4d224452801aced8b2f0aebe155379bb5d594381 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xf63e309818e4ea13782678ce6c31c1234fa61809 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xe0151763455a8a021e64880c238ba1cff3787ff0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x44ff8620b8ca30902395a7bd3f2407e1a091bf73 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x5640e0560e6afd6a9f4ddb41230d0201d181fea7 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x88ee7a3537667958d040216d9dc1752d1274d838 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x39d5313c3750140e5042887413ba8aa6145a9bd2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xba2a3dad197d6fee75471215efd5c30c8c854e11 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x3dd77d53f4fa9b3435b3a2ff6bb408771e6800e6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0xf929de51d91c77e42f5090069e0ad7a09e513c73 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x74885b4d524d497261259b38900f54e6dbad2210 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/optimism/0xc55e93c62874d8100dbd2dfe307edc1036ad5434 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x9c9e5fd8bbc25984b178fdce6117defa39d2db39 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0xaa53b93608c88ee55fad8db4c504fa20e52642ad - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x55cd6469f597452b5a7536e2cd98fde4c1247ee4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xfe550bffb51eb645ea3b324d772a19ac449e92c5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x314d7f9e2f55b430ef656fbb98a7635d43a2261e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x3b54eb78fc8103462f86976b06916fa46078b124 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x1d4731111bd2a50ab3dd5178574e6f3698270ffc - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x7a2c5e7788e55ec0a7ba4aeec5b3da322718fb5e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x814fe70e85025bec87d4ad3f3b713bdcaac0579b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x9b69667f602f15ef2d09a9a18489c788e327461e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x8808434a831efea81170a56a9ddc57cc9e6de1d8 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xe0c8b298db4cffe05d1bea0bb1ba414522b33c1b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x230ea9aed5d08afdb22cd3c06c47cf24ad501301 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x35d8949372d46b7a3d5a56006ae77b215fc69bc0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x535887989b9edffb63b1fd5c6b99a4d45443b49a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x9ee8c380e1926730ad89e91665ff27063b13c90a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xb8a914a00664e9361eae187468eff94905dfbc15 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xda2e903b0b67f30bf26bd3464f9ee1a383bbbe5f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0xd6cf874e24a9f5f43075142101a6b13735cdd424 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x8c92e38eca8210f4fcbf17f0951b198dd7668292 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x9a33406165f562e16c3abd82fd1185482e01b49a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x7f65323e468939073ef3b5287c73f13951b0ff5b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x5597ce42b315f29e42071d231dcd0158da35b77b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x0a14ef61afb32e5ca672e021784f71705ac14908 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x0f1cfd0bb452db90a3bfc0848349463010419ab2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xf3708859c178709d5319ad5405bc81511b72b9e9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xadf734e8d910d01e6528240898d895af6c22e2de - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x78a087d713be963bf307b18f2ff8122ef9a63ae9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x4287105ffac106eb98a71cab46586906181e35ff - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xb8e564b206032bbcda2c3978bc371da52152f72e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x3ecced5b416e58664f04a39dd18935eb71d33b15 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/celo/0x71e26d0e519d14591b9de9a0fe9513a398101490 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/celo/0x105d4a9306d2e55a71d2eb95b81553ae1dc20d7b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x420110d74c4c3ea14043a09e81fad53e1932f54c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xd6203889c22d9fe5e938a9200f50fdffe9dd8e02 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x0a6e7ba5042b38349e437ec6db6214aec7b35676 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x6f40d4a6237c257fff2db00fa0510deeecd303eb - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x3b991130eae3cca364406d718da22fa1c3e7c256 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x555907a0b5c32df0feb35401187aed60a9191d74 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x4947b72fed037ade3365da050a9be5c063e605a7 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xe9732d4b1e7d3789004ff029f032ba3034db059c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x556c3cbdca77a7f21afe15b17e644e0e98e64df4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x81f8f0bb1cb2a06649e51913a151f0e7ef6fa321 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xe69ccaaaea33ebfe5b76e0dd373cd9a1a31fd410 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x9aab071b4129b083b01cb5a0cb513ce7eca26fa5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x5ff0d2de4cd862149c6672c99b7edf3b092667a3 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x96a5399d07896f757bd4c6ef56461f58db951862 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x0d5105ec5bbbf17dba7a87e1aed2c2c15394a9e2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x00000000ea00f3f4000e7ed5ed91965b19f1009b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x5117f4ad0bc70dbb3b05bf39a1ec1ee40dd67654 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x4be87c766a7ce11d5cc864b6c3abb7457dcc4cc9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x16a500aec6c37f84447ef04e66c57cfc6254cf92 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x9f6abbf0ba6b5bfa27f4deb6597cc6ec20573fda - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/optimism/0x5465145a47260d5e715733997333a175d97285bb - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x93890f346c5d02c3863a06657bc72555dc72c527 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x1d1498166ddceee616a6d99868e1e0677300056f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x1d734a02ef1e1f5886e66b0673b71af5b53ffa94 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x4c3bf0a3de9524af68327d1d2558a3b70d17d42a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x36912b5cf63e509f18e53ac98b3012fa79e77bf5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x858c50c3af1913b0e849afdb74617388a1a5340d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x92dc4ab92eb16e781559e612f349916988013d5a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x548f93779fbc992010c07467cbaf329dd5f059b7 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xd1412d909f67b8db7505ddfcf26cf2303f4b1bb4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xfb1aaba03c31ea98a3eec7591808acb1947ee7ac - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x9562e2063122eaa4d7c2d786e7ca2610d70ca8b8 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x699ec925118567b6475fe495327ba0a778234aaa - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x37d299d9900209c3566254cfe59bfe6ff8f8c295 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x128f3e482f5bd5f08fe1b216e60ec0a6013deab9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x554fb3b6c1cf4a3cef49779ced321ca51c667d7d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x8578a8716013c390b95db73065922f512783e2cf - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xf5809f3348ff40906bb509f936aba43e6d1961ab - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x11920f139a3121c2836e01551d43f95b3c31159c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x45940000009600102a1c002f0097c4a500fa00ab - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x4debfb9ed639144cf1e401674af361ffffcefb58 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x0cfc9a713a5c17bc8a5ff0379467f6558bacd0e0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x0fd7a301b51d0a83fcaf6718628174d527b373b6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x1bc0c42215582d5a085795f4badbac3ff36d1bcb - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x3c4b6cd7874edc945797123fce2d9a871818524b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x64cb1bafc59bf93aeb90676885c63540cf4f4106 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xe8aae6251c6cf39927b0ff31399030c60bec798f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x3d1d651761d535df881740ab50ba4bd8a2ec2c00 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x8216e8143902a8fe0b676006bc25eb23829c123d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xce7de646e7208a4ef112cb6ed5038fa6cc6b12e3 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x947950bcc74888a40ffa2593c5798f11fc9124c4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x9f9bb3d5af7cc774f9b6adf66e32859b5a998952 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xba41ddf06b7ffd89d1267b5a93bfef2424eb2003 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x7a56e1c57c7475ccf742a1832b028f0456652f97 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x58d97b57bb95320f9a05dc918aef65434969c2b2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xc785698504a70be37d0e939a4c5326f8eddd5beb - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x4955f6641bf9c8c163604c321f4b36e988698f75 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x473f4068073cd5b2ab0e4cc8e146f9edc6fb52cc - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x16c22a91c705ec3c2d5945dbe2aca37924f1d2ed - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xac1d3d7a8878e655cbb063d58e453540641f4117 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xb72e76ccf005313868db7b48070901a44629da98 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0xa71e2738704e367798baa2755af5a10499634953 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x8697841b82c71fcbd9e58c15f6de68cd1c63fd02 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x7dff72693f6a4149b17e7c6314655f6a9f7c8b33 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/optimism/0x650af3c15af43dcb218406d30784416d64cfb6b2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/optimism/0x3c8b650257cfb5f272f799f5e2b4e65093a11a05 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0xe4feab21b42919c5c960ed2b4bdffc521e26881f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0xc3ec80343d2bae2f8e680fdadde7c17e71e114ea - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x9c2c5fd7b07e95ee044ddeba0e97a665f142394f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x0db510e79909666d6dec7f5e49370838c16d950f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x599f07567656e6961e20fa6a90685d393808c192 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x4f9fd6be4a90f2620860d680c0d4d5fb53d1a825 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x1185cb5122edad199bdbc0cbd7a0457e448f23c7 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xbaa5cc21fd487b8fcc2f632f3f4e8d37262a0842 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xb488fcb23333e7baa28d1dfd7b69a5d3a8bfeb3a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x2c8c89c442436cc6c0a77943e09c8daf49da3161 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x91ad1b44913cd1b8241a4ff1e2eaa198da6bf4c9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xa0a2e84f6f19c09a095d4a83ac8de5a32d303a13 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x1db0c569ebb4a8b57ac01833b9792f526305e062 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x8a638ea79f71f3b91bdc96bbdf9fb27c93013d60 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x731814e491571a2e9ee3c5b1f7f3b962ee8f4870 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x2c002ffec41568d138acc36f5894d6156398d539 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x33d13d537609841ce6c42d6fd775dc33e3833411 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x11d41056ff636107dd710ec4ea772490a710cdb7 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x2859e4544c4bb03966803b044a93563bd2d0dd4d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x77777feddddffc19ff86db637967013e6c6a116c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xfd418e42783382e86ae91e445406600ba144d162 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x4f7d2d728ce137dd01ec63ef7b225805c7b54575 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x2e44f3f609ff5aa4819b323fd74690f07c3607c4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x19e1f2f837a3b90ebd0730cb6111189be0e1b6d6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xd55210bb6898c021a19de1f58d27b71f095921ee - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x823556202e86763853b40e9cde725f412e294689 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x4d1c297d39c5c1277964d0e3f8aa901493664530 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x864cb5194722d5a1596f4be8b899916d30dad8d8 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x4a24b101728e07a52053c13fb4db2bcf490cabc3 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x79ead7a012d97ed8deece279f9bc39e264d7eef9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0xd566c529b33ecf15170f600d4b1ab12468c8efc6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x3b7e1ce09afe2bb3a23919afb65a38e627cfbe97 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0xaa404804ba583c025fa64c9a276a6127ceb355c6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x1a3acf6d19267e2d3e7f898f42803e90c9219062 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x6b2504a03ca4d43d0d73776f6ad46dab2f2a4cfd - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x98d59767cd1335071a4e9b9d3482685c915131e8 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x354d6890caa31a5e28b6059d46781f40880786a6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x30121d81f4407474a6d93f5c3060f14aaa098a61 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x5d9c2457a10d455e0ad8e28e40cc28eacf27a06a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xac27fa800955849d6d17cc8952ba9dd6eaa66187 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xe2b1dc2d4a3b4e59fdf0c47b71a7a86391a8b35a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xd2699f9fddc04d262a819808f561c153098c2408 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x23a96680ccde03bd4bdd9a3e9a0cb56a5d27f7c9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x3c5fdf0ee37d62c774025599e3b692d027746e24 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xf31e6d62bfc485857af2186eb3d8ee94b4379fed - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xcf6bb5389c92bdda8a3747ddb454cb7a64626c63 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xcf3c8be2e2c42331da80ef210e9b1b307c03d36a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x306227d964511a260d14563fbfa82aa75db404b2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xc00e94cb662c3520282e6f5717214004a7f26888 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x0c4785ee3ca8bf1fb90c772703210bd346aa3413 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xe41d2489571d322189246dafa5ebde1f4699f498 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x7fd4d7737597e7b4ee22acbf8d94362343ae0a79 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x3567aa22cd3ab9aef23d7e18ee0d7cf16974d7e6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xf107edabf59ba696e38de62ad5327415bd4d4236 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x06450dee7fd2fb8e39061434babcfc05599a6fb8 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x9cf0ed013e67db12ca3af8e7506fe401aa14dad6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x0bc529c00c6401aef6d220be8c6ea1667f6ad93e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x33333333fede34409fb7f67c6585047e1f653333 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0xc760f9782f8cea5b06d862574464729537159966 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x1b7ad346b6ff2d196daa8e78aed86baa6d7e3b02 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/optimism/0xff733b2a3557a7ed6697007ab5d11b79fdd1b76b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x05f52cc483c50c2a7e25a13dac17d736fa50f259 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0xafb755c5f2ea2aadbae693d3bf2dc2c35158dc04 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x101a023270368c0d50bffb62780f4afd4ea79c35 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x3562ddf1f5ce2c02ef109e9d5a72e2fdb702711d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x20d704099b62ada091028bcfc44445041ed16f09 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x161e113b8e9bbaefb846f73f31624f6f9607bd44 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xa6f774051dfb6b54869227fda2df9cb46f296c09 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x50ce4129ca261ccde4eb100c170843c2936bc11b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xbdf317f9c153246c429f23f4093087164b145390 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x08c81699f9a357a9f0d04a09b353576ca328d60d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xb33ff54b9f7242ef1593d2c9bcd8f9df46c77935 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x589864a9892b1a736ae70a91824ab4dc591fd8cd - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xd98832e8a59156acbee4744b9a94a9989a728f36 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x478e03d45716dda94f6dbc15a633b0d90c237e2f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x2676e4e0e2eb58d9bdb5078358ff8a3a964cedf5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x1c4cca7c5db003824208adda61bd749e55f463a3 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x749e5334752466cda899b302ed4176b8573dc877 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x63cb9a22cbc00bf9159429e9dede4b88c3dba8ce - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x2f20cf3466f80a5f7f532fca553c8cbc9727fef6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x2c24497d4086490e7ead87cc12597fb50c2e6ed6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x4f81837c2f4a189a0b69370027cc2627d93785b4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x252d223d0550bc6c137b003d90bc74f5341a2818 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x72ff5742319ef07061836f5c924ac6d72c919080 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x2ab0e9e4ee70fff1fb9d67031e44f6410170d00e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x8c907e0a72c3d55627e853f4ec6a96b0c8771145 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xc748673057861a797275cd8a068abb95a902e8de - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x62d7c4e3566f7f4033fc8e01b4d8e9bbc01c0760 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x109ba5f0230b7b39e4a8ab56e7361db89fa0e108 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x82a605d6d9114f4ad6d5ee461027477eeed31e34 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x0bb217e40f8a5cb79adf04e1aab60e5abd0dfc1e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xf411903cbc70a74d22900a5de66a2dda66507255 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xdd3b11ef34cd511a2da159034a05fcb94d806686 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x4ec1b60b96193a64acae44778e51f7bff2007831 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x78965b1c638a7ff408d1697a96d7b8e47bb7c75f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x60222751504796934bddee8218f9725f0c95d2c1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x1ccb4b14a11e0f2994a7ecbbd4cc69632f4c7c76 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xc7dcca0a3e69bd762c8db257f868f76be36c8514 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x9cdf242ef7975d8c68d5c1f5b6905801699b1940 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xadd39272e83895e7d3f244f696b7a25635f34234 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x0000000000c5dc95539589fbd24be07c6c14eca4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xcab254f1a32343f11ab41fbde90ecb410cde348a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x95ed629b028cf6aadd1408bb988c6d1daabe4767 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xa6c0c097741d55ecd9a3a7def3a8253fd022ceb9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xd888a5460fffa4b14340dd9fe2710cbabd520659 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x683989afc948477fd38567f8327f501562c955ac - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x946fb08103b400d1c79e07acccdef5cfd26cd374 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x7076de6ff1d91e00be7e92458089c833de99e22e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xadf7c35560035944e805d98ff17d58cde2449389 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x62b9c7356a2dc64a1969e19c23e4f579f9810aa7 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xd19b72e027cd66bde41d8f60a13740a26c4be8f3 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x66a1e37c9b0eaddca17d3662d6c05f4decf3e110 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xf477ac7719e2e659001455cdda0cc8f3ad10b604 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0xe16e2548a576ad448fb014bbe85284d7f3542df5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x330bd769382cfc6d50175903434ccc8d206dcae5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0xb08d8becab1bf76a9ce3d2d5fa946f65ec1d3e83 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/optimism/0x8b21e9b7daf2c4325bf3d18c1beb79a347fe902a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0xd9a9b4d466747e1ebcb7aeb42784452f40452367 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x306fd3e7b169aa4ee19412323e1a5995b8c1a1f4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x9cb74c8032b007466865f060ad2c46145d45553d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x333333c465a19c85f85c6cfbed7b16b0b26e3333 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xb9a5f238dc61eebe820060226c8143cd24624771 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xc48cddc6f2650bdb13dcf6681f61ba07209b5299 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xfb42da273158b0f642f59f2ba7cc1d5457481677 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x6f35720b272bf23832852b13ae9888c706e1a379 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x4498cd8ba045e00673402353f5a4347562707e7d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x1035ae3f87a91084c6c5084d0615cc6121c5e228 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x3639e6f4c224ebd1bf6373c3d97917d33e0492bb - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x8bfac1b375bf2894d6f12fb2eb48b1c1a7916789 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xd27c288fd69f228e0c02f79e5ecadff962e05a2b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x62ff28a01abd2484adb18c61f78f30fb2e4a6fdb - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x315b8c9a1123c10228d469551033440441b41f0b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x1d008f50fb828ef9debbbeae1b71fffe929bf317 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xeec468333ccc16d4bf1cef497a56cf8c0aae4ca3 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x20dd04c17afd5c9a8b3f2cdacaa8ee7907385bef - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x25e0a7767d03461eaf88b47cd9853722fe05dfd3 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x22af33fe49fd1fa80c7149773dde5890d3c76f3b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xbc1852f8940991d91bd2b09a5abb5e7b8092a16c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x06a63c498ef95ad1fa4fff841955e512b4b2198a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xeb6d78148f001f3aa2f588997c5e102e489ad341 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xa4dc5a82839a148ff172b5b8ba9d52e681fd2261 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xef22cb48b8483df6152e1423b19df5553bbd818b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xf83759099dc88f75fc83de854c41e0d9e83ada9b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x844c03892863b0e3e00e805e41b34527044d5c72 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xc9de725a4be9ab74b136c29d4731d6bebd7122e8 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x59e69094398afbea632f8bd63033bdd2443a3be1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x15f9eb4b9beafa9db35341c5694c0b6573809808 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x29cbd0510eec0327992cd6006e63f9fa8e7f33b7 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xcb1592591996765ec0efc1f92599a19767ee5ffa - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x26e550ac11b26f78a04489d5f20f24e3559f7dd9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x50327c6c5a14dcade707abad2e27eb517df87ab5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xc575bd129848ce06a460a19466c30e1d0328f52c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x47000a7b27a75d44ffadfe9d0b97fa04d569b323 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x2196b84eace74867b73fb003aff93c11fce1d47a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xc4441c2be5d8fa8126822b9929ca0b81ea0de38e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x421b05cf5ce28cb7347e73e2278e84472f0e4a88 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xcdbddbdefb0ee3ef03a89afcd714aa4ef310d567 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xf4308b0263723b121056938c2172868e408079d0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x8cedb0680531d26e62abdbd0f4c5428b7fdc26d5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x49fb8ad7578148e17c3ef0c344ce23a66ed372c4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x40e3d1a4b2c47d9aa61261f5606136ef73e28042 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x60d95823f795f1972dbdbcd886955095e36e04cd - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x761a3557184cbc07b7493da0661c41177b2f97fa - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x64c5cba9a1bfbd2a5faf601d91beff2dcac2c974 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x02f92800f57bcd74066f5709f1daa1a4302df875 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x291a50e611035b6562a2374b8b44de70aa8d7896 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x999faf0af2ff109938eefe6a7bf91ca56f0d07e1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x1a5b0aaf478bf1fda7b934c76e7692d722982a6d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x680447595e8b7b3aa1b43beb9f6098c79ac2ab3f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0xc1eb7689147c81ac840d4ff0d298489fc7986d52 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x60bf4e7cf16ff34513514b968483b54beff42a81 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/optimism/0x5df7abe3c51c01dcf6d1f1f9a0ab4dc3759869b9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0xfe049f59963545bf5469f968e04c9646d6e2c2c5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xc0041ef357b183448b235a8ea73ce4e4ec8c265f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x12e377989a87da0f9b9166f0f875c9069eaa776c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xfc48314ad4ad5bd36a84e8307b86a68a01d95d9c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x0521aaa7c96e25afee79fdd4f1bb48f008ae4eac - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x7a5f5ccd46ebd7ac30615836d988ca3bd57412b3 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x20ef84969f6d81ff74ae4591c331858b20ad82cd - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x4d70f1058b73198f12a76c193aef5db5dd75babd - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x9239e9f9e325e706ef8b89936ece9d48896abbe3 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xca73ed1815e5915489570014e024b7ebe65de679 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x79dacb99a8698052a9898e81fdf883c29efb93cb - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x2f6c17fa9f9bc3600346ab4e48c0701e1d5962ae - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xba5e66fb16944da22a62ea4fd70ad02008744460 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x2b0772bea2757624287ffc7feb92d03aeae6f12d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xc841b4ead3f70be99472ffdb88e5c3c7af6a481a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xf8f97a79a3fa77104fab4814e3ed93899777de0d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xf0d7cb351589c4b1520bf8d31afc87f7fb839c85 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x511ef9ad5e645e533d15df605b4628e3d0d0ff53 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x625bb9bb04bdca51871ed6d07e2dd9034e914631 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x291a8da3c42b7d7f00349d6f1be3c823a2b3fca4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xf09034487c84954d49ae04bf6817148ffc2edb83 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x1e2093ab84768948c6176db5ad98c909ce97f368 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x64fcc3a02eeeba05ef701b7eed066c6ebd5d4e51 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xc796e499cc8f599a2a8280825d8bda92f7a895e0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xbdb0e1c40a76c5113a023d685b419b90b01e3d61 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x76c71f1703fbf19ffdcf3051e1e684cb9934510f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xc655c331d1aa7f96c252f1f40ce13d80eac53504 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x85645b86243886b7c7c1da6288571f8bea6fc035 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x623cd3a3edf080057892aaf8d773bbb7a5c9b6e9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xab964f7b7b6391bd6c4e8512ef00d01f255d9c0d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x6112b8714221bbd96ae0a0032a683e38b475d06c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xc2eeca228ebac45c339cc5e522dd3a10638155f1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x018dd3a0dd7f213cc822076b3800816d3ce1ed86 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x446c9033e7516d820cc9a2ce2d0b7328b579406f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x73a15fed60bf67631dc6cd7bc5b6e8da8190acf5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x4448726b23483927c492f09c1dbfdffd3967b452 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xeeacc51af745846ddf46012b46c6910ea9b12898 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x25931894a86d47441213199621f1f2994e1c39aa - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x94a8b4ee5cd64c79d0ee816f467ea73009f51aa0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x1a4e7febd24b6689704b10685857d8b30885f05e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x8248270620aa532e4d64316017be5e873e37cc09 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xca7af58da871736994ce360f51ec6cd28351a3df - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xca4f53e6117623992126a9a45ce61682fe8678df - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x79bbf4508b1391af3a0f4b30bb5fc4aa9ab0e07c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0xd2a530170d71a9cfe1651fb468e2b98f7ed7456b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0xbf7970d56a150cd0b60bd08388a4a75a27777777 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0xb87904db461005fc716a6bf9f2d451c33b10b80b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x708383ae0e80e75377d664e4d6344404dede119a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x2c89bbc92bd86f8075d1decc58c7f4e0107f286b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x288f4eb27400fa220d14b864259ad1b7f77c1594 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x2d57c47bc5d2432feeedf2c9150162a9862d3ccf - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x91da780bc7f4b7cf19abe90411a2a296ec5ff787 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xb89d354ad1b0d95a48b3de4607f75a8cd710c1ba - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x79bbf4508b1391af3a0f4b30bb5fc4aa9ab0e07c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x6263ad921e11ab47ae85f1daa725b8b3581baed3 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x57edc3f1fd42c0d48230e964b1c5184b9c89b2ed - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xa608512bbc9934e4b1ddecf0f5fb38b6ad93308d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x9e6a46f294bb67c20f1d1e7afb0bbef614403b55 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x767a739d1a152639e9ea1d8c1bd55fdc5b217d7f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xfbecd19292b1effeaa7b2e61f5101ddb6744a1fb - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xf04d220b8136e2d3d4be08081dbb565c3c302ffd - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xce1eab31756a48915b7e7bb79c589835aac6242d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xd1d7aa941c71fd95e9d31bbd81937b3e71bd6231 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x53ae20d42e16626dc41c7842d9ce876358082370 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x5ab3d4c385b400f3abb49e80de2faf6a88a7b691 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xc438b0c0e80a8fa1b36898d1b36a3fc2ec371c54 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xbe35071605277d8be5a52c84a66ab1bc855a758d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x8899ec96ed8c96b5c86c23c3f069c3def75b6d97 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xa2cd3d43c775978a96bdbf12d733d5a1ed94fb18 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xb6aaa0efdfac186652e3b31a6f07a9a74d1b5a75 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x6b0b3a982b4634ac68dd83a4dbf02311ce324181 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x32f4768fc4a238a58fc9da408d9a0da4333012e4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x0721b3c9f19cfef1d622c918dcd431960f35e060 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x0caadd427a6feb5b5fc1137eb05aa7ddd9c08ce9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x77b7787a09818502305c95d68a2571f090abb135 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x9f07f8a82cb1af1466252e505b7b7ddee103bc91 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0xdb298285fe4c5410b05390ca80e8fbe9de1f259b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x4cfe63294dac27ce941d42a778a37f2b35fea21b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0xdf7837de1f2fa4631d716cf2502f8b230f1dcc32 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0xce899f26928a2b21c6a2fddd393ef37c61dba918 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x21e00ff5374a0b803e0dc13a72800aca95b4b09e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xe4a7b54c0a30da69c04dc54b89868c185ff382bc - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xf857b2764095b9a5f57c3e71f82f297fe4e45334 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x9c632e6aaa3ea73f91554f8a3cb2ed2f29605e0c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x097b1b242d3ed90e191c5f83a62f41abe16f6ceb - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x504a26cf29674bc77a9341e73f88ccecc864034c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x0c1dc73159e30c4b06170f2593d3118968a0dca5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x42e07fa3d31190731368ca2f88d12d80139dca42 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x3a46ed8fceb6ef1ada2e4600a522ae7e24d2ed18 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x4b361e60cf256b926ba15f157d69cac9cd037426 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xdd3acdbdc7b358df453a6cb6bca56c92aa5743aa - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x78ec15c5fd8efc5e924e9eebb9e549e29c785867 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x164ffdae2fe3891714bc2968f1875ca4fa1079d0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x98f4779fccb177a6d856dd1dfd78cd15b7cd2af5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xabe8e5cabe24cb36df9540088fd7ce1175b9bc52 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x80a78a9b6b1272fdb612b39181bf113706024875 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x7da2641000cbb407c329310c461b2cb9c70c3046 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xfeac2eae96899709a43e252b6b92971d32f9c0f9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x0f51bb10119727a7e5ea3538074fb341f56b09ad - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x15700b564ca08d9439c58ca5053166e8317aa138 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x59a529070fbb61e6d6c91f952ccb7f35c34cf8aa - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x1010107b4757c915bc2f1ecd08c85d1bb0be92e0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x3845badade8e6dff049820680d1f14bd3903a5d0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x6f365eb3686ee95bdefbae71f1728d62c0af7ab1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x85f17cf997934a597031b2e18a9ab6ebd4b9f6a4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xcc4304a31d09258b0029ea7fe63d032f52e44efe - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x1196c6704789620514fd25632abe15f69a50bc4f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x7475fa4c36344f1d633964f02564f37162299194 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x543ba622733bc9a7bfadd1d07b6c35ae1f9659d9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x20fd4c5396f7d9686f9997e0f10991957f7112fc - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x0b3ae50babe7ffa4e1a50569cee6bdefd4ccaee0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xf1bb41f9ed87e6c7e1f70e921b7b4bee1df7ae9c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x97ad75064b20fb2b2447fed4fa953bf7f007a706 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xad0d1436dd45dbd6d8e50ac82240b72f52d7ea89 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xa0ef786bf476fe0810408caba05e536ac800ff86 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xb5130f4767ab0acc579f25a76e8f9e977cb3f948 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xbb0e17ef65f82ab018d8edd776e8dd940327b28b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x8a60e489004ca22d775c5f2c657598278d17d9c2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x4c1746a800d224393fe2470c70a35717ed4ea5f1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xf34960d9d60be18cc1d5afc1a6f012a723a28811 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x9be89d2a4cd102d8fecc6bf9da793be995c22541 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x4eddb15a0abfa2c349e8065af9214e942d9a6d36 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x37a645648df29205c6261289983fb04ecd70b4b3 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x1cd9a56c8c2ea913c70319a44da75e99255aa46f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x6b43732a9ae9f8654d496c0a075aa4aa43057a0b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0xfea7a6a0b346362bf88a9e4a88416b77a57d6c2a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x6d7187220f769bde541ff51dd37ee07416f861d2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x2f3e306d9f02ee8e8850f9040404918d0b345207 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0xecdcb5b88f8e3c15f95c720c51c71c9e2080525d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xa1832f7f4e534ae557f9b5ab76de54b1873e498b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x514d8e8099286a13486ef6c525c120f51c239b52 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x4837b18a6d7af6159c8665505b90a2ed393255e0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x6bfdb6f4e65ead27118592a41eb927cea6956198 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xd7efb00d12c2c13131fd319336fdf952525da2af - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xf1a7000000950c7ad8aff13118bb7ab561a448ee - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x56cfc19d8cbf7d417d370844249be9cb2d2e19a1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x65c101e95d7dd475c7966330fa1a803205ff92ab - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x0c5fa0e07949f941a6c2c29a008252db1527d6ee - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/optimism/0xfdb794692724153d1488ccdbe0c56c252596735f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0xf7e78d9c4c74df889a83c8c8d6d05bf70ff75876 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x8fa62d23fb6359c1e1685dbfa9b63ef27ecdb612 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xf1a7000000950c7ad8aff13118bb7ab561a448ee - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xacfe6019ed1a7dc6f7b508c02d1b04ec88cc21bf - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x9704d2adbc02c085ff526a37ac64872027ac8a50 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x3bf9a2a798c9b122747344da0276d30a267a80dc - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x64baa63f3eedf9661f736d8e4d42c6f8aa0cda71 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xbe0d3526fc797583dada3f30bc390013062a048b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xf5d8015d625be6f59b8073c8189bd51ba28792e1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x0ff6ffcfda92c53f615a4a75d982f399c989366b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x841a3083074b1a40b644bf2ba2491a731b6da277 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xb01dd87b29d187f3e3a4bf6cdaebfb97f3d9ab98 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x667102bd3413bfeaa3dffb48fa8288819e480a88 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x3073f7aaa4db83f95e9fff17424f71d4751a3073 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x32b86b99441480a7e5bd3a26c124ec2373e3f015 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xbf2e353f5db1a01e4e7f051222c666afc81b2574 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x960fce8724aa127184b6d13af41a711755236c77 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x9ea59db651a3c79a8d52a394a49da8e9a214d6ae - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x95987b0cdc7f65d989a30b3b7132a38388c548eb - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x707f635951193ddafbb40971a0fcaab8a6415160 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0xcafcd85d8ca7ad1e1c6f82f651fa15e33aefd07b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0xdd1ddd4d978ac0baef4bfa9c7e91853bfce90f11 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x2ab445c24c96db13383bb34678adae50c43b4baa - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0xc157ee77518769b8009642f68a8d6a500ff59d53 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x90ec58ef4cc9f37b96de1e203b65bd4e6e79580e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xd47f3e45b23b7594f5d5e1ccfde63237c60be49e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xb3b32f9f8827d4634fe7d973fa1034ec9fddb3b3 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xd758916365b361cf833bb9c4c465ecd501ddd984 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xbc7755a153e852cf76cccddb4c2e7c368f6259d8 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xb5130f4767ab0acc579f25a76e8f9e977cb3f948 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x39d5313c3750140e5042887413ba8aa6145a9bd2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x86bb94ddd16efc8bc58e6b056e8df71d9e666429 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xa4c3497b57c8b6d510f3707a1e9694fd791f45fb - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x1f3af095cda17d63cad238358837321e95fc5915 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x0688977ae5b10075f46519063fd2f03adc052c1f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xccb365d2e11ae4d6d74715c680f56cf58bf4bf10 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x643c4e15d7d62ad0abec4a9bd4b001aa3ef52d66 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x8e729198d1c59b82bd6bba579310c40d740a11c2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0xd4fe6e1e37dfcf35e9eeb54d4cca149d1c10239f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0xaeac3b55c3522157ecda7ec8fcb86c832faa28af - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x46777c76dbbe40fabb2aab99e33ce20058e76c59 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0xa78d8321b20c4ef90ecd72f2588aa985a4bdb684 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0xaef5bbcbfa438519a5ea80b4c7181b4e78d419f2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x82e64f49ed5ec1bc6e43dad4fc8af9bb3a2312ee - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/optimism/0x484c2d6e3cdd945a8b2df735e079178c1036578c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/optimism/0x46777c76dbbe40fabb2aab99e33ce20058e76c59 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/optimism/0x1e925de1c68ef83bd98ee3e130ef14a50309c01b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x300211def2a644b036a9bdd3e58159bb2074d388 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x98d0baa52b2d063e780de12f615f963fe8537553 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x67543cf0304c19ca62ac95ba82fd4f4b40788dc1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x938171227ece879267122a36847b219cbd3b9d47 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x6967f0974d76d34e140cae27efea32cdf546b58e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x4b12507e171970b3acd48edfeb5bd1c676e61280 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xb4d4ff3fcbd6965962a79229aa94631d394217cb - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xeb476e9ab6b1655860b3f40100678d0c1cedb321 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xd5eaaac47bd1993d661bc087e15dfb079a7f3c19 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x6d5ad1592ed9d6d1df9b93c793ab759573ed6714 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xa18bbdcd86e4178d10ecd9316667cfe4c4aa8717 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xf78d2e7936f5fe18308a3b2951a93b6c4a41f5e2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x7ff7fa94b8b66ef313f7970d4eebd2cb3103a2c0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x40e3eddf6d253bb734381a309437428f121c594b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x56072c95faa701256059aa122697b133aded9279 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x881ba05de1e78f549cc63a8f6cabb1d4ad32250d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x6c3ea9036406852006290770bedfcaba0e23a0e8 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x6fb3e0a217407efff7ca062d46c26e5d60a14d69 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xd89cc9d79ad3c49e2cd477a8bbc8e63dee53f82e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x9d0bfe46891573c12021942c7d28f15ebb641988 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x46777c76dbbe40fabb2aab99e33ce20058e76c59 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x1f1c695f6b4a3f8b05f2492cef9474afb6d6ad69 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x3c8cd0db9a01efa063a7760267b822a129bc7dca - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x060cb087a9730e13aa191f31a6d86bff8dfcdcc0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xbf1aea8670d2528e08334083616dd9c5f3b087ae - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x9b8df6e244526ab5f6e6400d331db28c8fdddb55 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x4b6104755afb5da4581b81c552da3a25608c73b8 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x12b4356c65340fb02cdff01293f95febb1512f3b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x08e8ac8c4bca64503f774d2c40c911e8a3ffcc12 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x47a1eb0b825b73e6a14807beaecafef199d5477c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x103071da56e7cd95b415320760d6a0ddc4da1ca5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xc43c6bfeda065fe2c4c11765bf838789bd0bb5de - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x5732046a883704404f284ce41ffadd5b007fd668 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x8292bb45bf1ee4d140127049757c2e0ff06317ed - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x2d8ea194902bc55431420bd26be92b0782dce91d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0xd1f9c58e33933a993a3891f8acfe05a68e1afc05 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x17d70172c7c4205bd39ce80f7f0ee660b7dc5a23 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x624e2e7fdc8903165f64891672267ab0fcb98831 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xbf388570ebd5b88bfc7cd21ec469813c15f453a3 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x44551ca46fa5592bb572e20043f7c3d54c85cad7 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x2133031f5acbc493572c02f271186f241cd8d6a5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xcb6ab91007fe165c81b1e672007361cce9995fd1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xb4fde59a779991bfb6a52253b51947828b982be3 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x09be1692ca16e06f536f0038ff11d1da8524adb1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xc0634090f2fe6c6d75e61be2b949464abb498973 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xd51827754a56860f04acd1d2699b049b026a5925 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xa2e3356610840701bdf5611a53974510ae27e2e1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x944824290cc12f31ae18ef51216a223ba4063092 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x10dea67478c5f8c5e2d90e5e9b26dbe60c54d800 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x2f42b7d686ca3effc69778b6ed8493a7787b4d6e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xc669928185dbce49d2230cc9b0979be6dc797957 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x5485a469faea1492191cfce7528ab6e58135aa4d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x8e0e57dcb1ce8d9091df38ec1bfc3b224529754a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x1151cb3d861920e07a38e03eead12c32178567f6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x0305f515fa978cf87226cf8a9776d25bcfb2cc0b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x88800092ff476844f74dc2fc427974bbee2794ae - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xe76c6c83af64e4c60245d8c7de953df673a7a33d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x90685e300a4c4532efcefe91202dfe1dfd572f47 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xa849eaae994fb86afa73382e9bd88c2b6b18dc71 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xbb2a93afcf5d3af8ae28dd50d6c18556ea532c5a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xbad6c59d72d44512616f25b3d160c79db5a69ddf - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x4604151d4c98d1eea200b0d6bffb79a2613182aa - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xa93d86af16fe83f064e3c0e2f3d129f7b7b002b0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x419c4db4b9e25d6db2ad9691ccb832c8d9fda05e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x004e9c3ef86bc1ca1f0bb5c7662861ee93350568 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x68749665ff8d2d112fa859aa293f07a622782f38 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x4da27a545c0c5b758a6ba100e3a049001de870f5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x820802fa8a99901f52e39acd21177b0be6ee2974 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x3ec2156d4c0a9cbdab4a016633b7bcf6a8d68ea2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x937a1cfaf0a3d9f5dc4d0927f72ee5e3e5f82a00 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x2e2cc4dfce60257f091980631e75f5c436b71c87 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x65553f5c85c78b977668cec098ec09475099aa61 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x04175b1f982b8c8444f238ac0aae59f029e21099 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x1c93d155bd388241f9ab5df500d69eb529ce9583 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x45d9c101a3870ca5024582fd788f4e1e8f7971c3 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x1986cc18d8ec757447254310d2604f85741aa732 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xb3621cd34803cf7065dcb0d5bfb0f56c1834a063 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x570b1533f6daa82814b25b62b5c7c4c55eb83947 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x43c5034469bce262d32f64c5e7f9f359f5b1495f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xbdf43ecadc5cef51b7d1772f722e40596bc1788b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x80ac24aa929eaf5013f6436cda2a7ba190f5cc0b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x9813037ee2218799597d83d4a5b6f3b6778218d9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x88909d489678dd17aa6d9609f89b0419bf78fd9a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x4274cd7277c7bb0806bd5fe84b9adae466a8da0a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xa0246c9032bc3a600820415ae600c6388619a14d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xed04915c23f00a313a544955524eb7dbd823143d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0xaf7e3f16d747e77e927dc94287f86eb95a64d83d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x290f057a2c59b95d8027aa4abf31782676502071 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x46777c76dbbe40fabb2aab99e33ce20058e76c59 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x9e12735d77c72c5c3670636d428f2f3815d8a4cb - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x6bb7a212910682dcfdbd5bcbb3e28fb4e8da10ee - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xf2e244d4020c182e8e2c936d4055e3f0e578064f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xe3cf8dbcbdc9b220ddead0bd6342e245daff934d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xebcda5b80f62dd4dd2a96357b42bb6facbf30267 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xdc06717f367e57a16e06cce0c4761604460da8fc - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xd5369a3cac0f4448a9a96bb98af9c887c92fc37b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x3199a64bc8aabdfd9a3937a346cc59c3d81d8a9a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xff7d6a96ae471bbcd7713af9cb1feeb16cf56b41 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/NATIVE - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/NATIVE - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/NATIVE - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/polygon/0x0000000000000000000000000000000000001010 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/optimism/NATIVE - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/unichain/NATIVE - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/NATIVE - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x68037790a0229e9ce6eaa8a99ea92964106c4703 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x28d38df637db75533bd3f71426f3410a82041544 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/avalanche/NATIVE - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/avalanche/0xb97ef9ef8734c71904d8002f8b6bc66dd9c48a6e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/avalanche/0x49d5c2bdffac6ce2bfdb6640f4f80f226bc10bab - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x437cc33344a0b27a429f795ff6b469c72698b291 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x30c7235866872213f68cb1f08c37cb9eccb93452 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xdc035d45d973e3ec169d2276ddab16f1e407384f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/unichain/0x078d782b760474a361dda0af3839290b0ef57ad6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/unichain/0x9151434b16b9763660705744891fa906f660ecc5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xdb99b0477574ac0b2d9c8cec56b42277da3fdb82 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xd769d56f479e9e72a77bb1523e866a33098feec5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/unichain/0x927b51f251480a681271180da4de28d44ec4afb8 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/unichain/0xc02fe7317d4eb8753a02c35fe019786854a92001 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/unichain/0x7dcc39b4d1c53cb31e1abc0e358b43987fef80f7 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/unichain/0x2416092f143378750bb29b79ed961ab195cceea5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xfa2b947eec368f42195f24f36d2af29f7c24cec2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/unichain/0xc3eacf0612346366db554c991d7858716db09f58 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x85e90a5430af45776548adb82ee4cd9e33b08077 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/optimism/0xef4461891dfb3ac8572ccf7c794664a8dd927945 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x0f81001ef0a83ecce5ccebf63eb302c70a39a654 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x1111111111166b7fe7bd91427724b487980afc69 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/unichain/0x8f187aa05619a017077f5308904739877ce9ea21 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/avalanche/0x152b9d0fdc40c096757f570a51e494bd4b943e50 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x5a70be406ce7471a44f0183b8d7091f4ad751db5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xbc396689893d065f41bc2c6ecbee5e0085233447 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x7eb4db4dddb16a329c5ade17a8a0178331267e28 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/worldchain/NATIVE - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/worldchain/0x79a02482a880bce3f13e09da970dc34db4cd24d1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xf816507e690f5aa4e29d164885eb5fa7a5627860 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xd3f68c6e8aee820569d58adf8d85d94489315192 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x4d0528598f916fd1d8dc80e5f54a8feedcfd4b18 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xa0c56a8c0692bd10b3fa8f8ba79cf5332b7107f9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x8d0d000ee44948fc98c9b98a4fa4921476f08b0d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xecca809227d43b895754382f1fd871628d7e51fb - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x8d0d000ee44948fc98c9b98a4fa4921476f08b0d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x9cb41fd9dc6891bae8187029461bfaadf6cc0c69 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/worldchain/0x2cfc85d8e48f8eab294be644d9e25c3030863003 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xe6df05ce8c8301223373cf5b969afcb1498c5528 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/unichain/0x0555e30da8f98308edb960aa94c0db47230d2b9c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x4d4574f50dd8b9dbe623cf329dcc78d76935e610 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x7d6fcb3327d7e17095fa8b0e3513ac7a3564f5e1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x7300b37dfdfab110d83290a29dfb31b1740219fe - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xbf8566956b4e2d8beb90c4c19dbb8c67a9290c36 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x87d00066cf131ff54b72b134a217d5401e5392b6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xdac991621fd8048d9f235324780abd6c3ad26421 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xe50e3d1a46070444f44df911359033f2937fcc13 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xa776a95223c500e81cb0937b291140ff550ac3e4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x4f1aac70b303818ddd0823570af3bb46681d9bd8 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x95034f653d5d161890836ad2b6b8cc49d14e029a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x3f160760535eb715d5809a26cf55408a2d9844c1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xde04da55b74435d7b9f2c5c62d9f1b53929b09aa - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x595e21b20e78674f8a64c1566a20b2b316bc3511 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xe6f98920852a360497dbcc8ec895f1bb1f7c8df4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xd6b48ccf41a62eb3891e58d0f006b19b01d50cca - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x926759a8eaecfadb5d8bdc7a9c7b193c5085f507 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xd86e6ef14b96d942ef0abf0720c549197ea8c528 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x30c60b20c25b2810ca524810467a0c342294fc61 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x6444c6c2d527d85ea97032da9a7504d6d1448ecf - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x0f7dc5d02cc1e1f5ee47854d534d332a1081ccc8 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x3fefe29da25bea166fb5f6ade7b5976d2b0e586b - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xc9ccbd76c2353e593cc975f13295e8289d04d3bb - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x39702843a6733932ec7ce0dde404e5a6dbd8c989 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x4fa7c69a7b69f8bc48233024d546bc299d6b03bf - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xe0cd4cacddcbf4f36e845407ce53e87717b6601d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xe0b7ad7f8f26e2b00c8b47b5df370f15f90fcf48 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/avalanche/0x9702230a8ea53601f5cd2dc00fdbc13d4df4a8c7 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x6a72d3a87f97a0fee2c2ee4233bdaebc32813d7a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xe868084cf08f3c3db11f4b73a95473762d9463f7 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xb035723d62e0e2ea7499d76355c9d560f13ba404 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xad8c787992428cd158e451aab109f724b6bc36de - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xc20059e0317de91738d13af027dfc4a50781b066 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xf39e4b21c84e737df08e2c3b32541d856f508e48 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x30a538effd91acefb1b12ce9bc0074ed18c9dfc9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x95af4af910c28e8ece4512bfe46f1f33687424ce - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x9ab778f84b2397c7015f7e83d12eee47d4c26694 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x45e02bc2875a2914c4f585bbf92a6f28bc07cb70 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xcab84bc21f9092167fcfe0ea60f5ce053ab39a1e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x2f299be3b081e8cd47dc56c1932fcae7a91b5dcd - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x5f5668d7c748fc1a17540c3a7f9245d8cea10c29 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xf9902edfca4f49dcaebc335c73aebd82c79c2886 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xc96de26018a54d51c097160568752c4e3bd6c364 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x0e7779e698052f8fe56c415c3818fcf89de9ac6d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x3e05284ff11a92d66e44b6b3ea70533729670257 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x6b785a0322126826d8226d77e173d75dafb84d11 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x226a2fa2556c48245e57cd1cba4c6c9e67077dd2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x1d58e204ca59328007469a614522903d69dc0a4c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xea87148a703adc0de89db2ac2b6b381093ae8ee0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xcaa1e525acb44aec4e0d17a0e2467aa3ea7ee3a6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xa4a2e2ca3fbfe21aed83471d28b6f65a233c6e00 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/celo/0xd221812de1bd094f35587ee8e174b07b6167d9af - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x7b10d50b5885be4c7985a88408265c109bd1eec8 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x4a0aaf171446dda0ed95295c46820e2015a28b07 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x00c83aecc790e8a4453e5dd3b0b4b3680501a7a7 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/unichain/0x07f1508a8fe276fb96b9fcf80ace41eb2abddf81 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x6555255b8ded3c538cb398d9e36769f45d7d3ea7 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xc299004a310303d1c0005cb14c70ccc02863924d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xe3225e11cab122f1a126a28997788e5230838ab9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x10ee9f68ee4e4d311e854ae14c53f5b25a917f85 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xaccfd598ef801178ed6c816c234b16ec51ae9f32 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x1aecab957bad4c6e36dd29c3d3bb470c4c29768a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x75231f58b43240c9718dd58b4967c5114342a86c - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x61fac5f038515572d6f42d4bcb6b581642753d50 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xd5c3a723e63a0ecab81081c26c6a3c4b2634bf85 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xf8f331dfa811132c43c308757cd802ca982b7211 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x2c3a8ee94ddd97244a93bc48298f97d2c412f7db - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x642ee4b1f054da33bc6003ad0278f9a27478caf5 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xb994882a1b9bd98a71dd6ea5f61577c42848b0e8 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xda5e1988097297dcdc1f90d4dfe7909e847cbef6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xdac32deb60c817ee1cd8fc2bb2a7cda2ce1732d0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xd955c9ba56fb1ab30e34766e252a97ccce3d31a6 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/unichain/0x89824f7609107efe6f8088cd89a620c8edbeed6e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xa9616e5e23ec1582c2828b025becf3ef610e266f - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/unichain/0xbde8a5331e8ac4831cf8ea9e42e229219eafab97 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x000ae314e2a2172a039b26378814c252734f556a - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xb4c6fedd984bc983b1a758d0875f1ea34f81a6af - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xc50673edb3a7b94e8cad8a7d4e0cd68864e33edf - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x2fd23a21c52fff0535328a7177da1fb31b8a819e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/unichain/0x15d0e0c55a3e7ee67152ad7e89acf164253ff68d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x11dc28d01984079b7efe7763b533e6ed9e3722b9 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x1789e0043623282d5dcc7f213d703c6d8bafbb04 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xfa35e2250e376c23955247383dc32c79082e7fcc - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/native - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/native - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/native - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/unichain/native - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/avalanche/native - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x405fbc9004d857903bfd6b3357792d71a50726b0 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/optimism/native - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/native - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x696f9436b67233384889472cd7cd58a6fb5df4f1 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/worldchain/native - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0x8dedf84656fa932157e27c060d8613824e7979e3 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/bnb/0xacf5a368ec5bb9e804c8ac0b508daa5a21c92e13 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0xa64a1b5b0a5ce578a8c6bca10cbe36d83713d170 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/avalanche/0x211cc4dd073734da055fbf44a2b4667d5e5fe5d2 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/arbitrum/0x0a1a1a107e45b7ced86833863f482bc5f4ed82ef - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0x52a8845df664d76c69d2eea607cd793565af42b8 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x9c4315eb7ba91b7bdc1727ad5f76ea4ac8875506 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/solana/AzVCECC5zr21ikYQne4Xtoh3SVg848duPHUHb3M7bonk - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/solana/8avjtjHAHFqp4g2RR9ALAGBpSTqKPZR8nRbzSTwZERA - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/solana/8ZHE4ow1a2jjxuoMfyExuNamQNALv5ekZhsBn5nMDf5e - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/solana/native - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/solana/EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/solana/HUSTLFV3U5Km8u66rMQExh4nLy7unfKHedEXVK1WgSAG - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/solana/3uXACfojUrya7VH51jVC1DCHq3uzK4A7g469Q954LABS - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/solana/4NGbC4RRrUjS78ooSN53Up7gSg4dGrj6F6dxpMWHbonk - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/solana/6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/solana/goodX4LG92UAcRdFRUykfP2fAfzQAVttrToEPxtxSkp - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/solana/7kN5FQMD8ja4bzysEgc5FXmryKd6gCgjiWnhksjHCFb3 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/solana/HeLp6NuQkmYB4pYWo2zYs22mESHXPQYzXbB8n4V98jwC - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/solana/27G8MtK7VtTcCHkpASjSDdkWWYfoqT6ggEuKidVJidD4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/solana/FViMp5phQH2bX81S7Yyn1yXjj3BRddFBNcMCbTH8FCze - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/solana/QdyjMr627PR7NtWdcEcgFmDm5haBVUWEcj4jdM4boop - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/solana/Dz9mQ9NzkBcCsuGPFJ3r1bS4wgqKMHBPiVuniW8Mbonk - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/solana/cbbtcf3aa214zXHbiAZQwf4122FBYbraNdFqgw4iMij - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/solana/3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/solana/3xypwTgs9nWgjc6nUBiHmMb36t2PwL3SwCZkEQvW8FTX - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xaca92e438df0b2401ff60da7e4337b687a2435da - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/solana/USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/solana/7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/solana/KMNo3nJsBXfcpJTVhZcXLW7RmTwTt4GVFE7suUBo9sS - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/solana/J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/solana/2u1tszSeqZ3qBWF3uNGPFc8TzMk2tdiwknnRMWGWjGWH - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/solana/UwU8RVXB69Y6Dcju6cN2Qef6fykkq6UUNpB15rZku6Z - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/solana/AvZZF1YaZDziPY2RCK4oJrRVrbN3mTD9NL24hPeaZeUj - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/solana/HzwqbKZw8HxMN6bF2yFZNrht3c2iXXzpKcFu7uBEDKtr - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/solana/Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/solana/9tqjeRS1swj36Ee5C1iGiwAxjQJNGAVCzaTLwFY8bonk - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/solana/2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/solana/mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/solana/CASHx9KJUStyftLFWGvEVf59SGeG9sh5FfcnZMVPCASH - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/solana/5oVNBeEEQvYi1cX3ir8Dx5n1P7pdxydbGF2X4TxVusJm - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/solana/76rTxzztXjJe7AUaBi7jQ5J61MFgpQgB4Cc934sWbonk - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xa37f4d2dc3d3c136b68fcf52be3afa09d4dfc38d - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/ethereum/0xfab99fcf605fd8f4593edb70a43ba56542777777 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/base/0x8a910ea80fc09d5b5a2120521a39b67980df0bc4 - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/solana/jupSoLaHXQiZZTSfEWMTRRgpnyFm8f6sZdosWBjx93v - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/solana/Ey59PH7Z4BFU4HjyKnyMdWt5GGN76KazTAwQihoUXRnk - 2025-10-17T22:04:33.647Z - 0.8 - - - https://app.uniswap.org/explore/tokens/solana/CARDSccUMFKoPRZxt5vt3ksUbxEFEcnZ3H2pd3dKxYjp - 2025-10-17T22:04:33.647Z + https://app.uniswap.org/explore/tokens/base/0x290f057a2c59b95d8027aa4abf31782676502071 + 2025-03-20T21:28:30.528Z 0.8 diff --git a/apps/web/scripts/start-anvil.sh b/apps/web/scripts/start-anvil.sh new file mode 100755 index 00000000000..1abc48436c7 --- /dev/null +++ b/apps/web/scripts/start-anvil.sh @@ -0,0 +1,14 @@ +#!/bin/bash +FORK_URL=$1 +PORT=${2:-8545} +EXTRA_FLAGS=${3:-} + +RUST_LOG=debug anvil \ + --print-traces \ + --disable-code-size-limit \ + --disable-min-priority-fee \ + --no-rate-limit \ + --hardfork prague \ + --fork-url "$FORK_URL" \ + --port "$PORT" \ + ${EXTRA_FLAGS} diff --git a/apps/web/src/appGraphql/data/apollo/client.ts b/apps/web/src/appGraphql/data/apollo/client.ts index bbd4eba5d0c..2d129fd1d35 100644 --- a/apps/web/src/appGraphql/data/apollo/client.ts +++ b/apps/web/src/appGraphql/data/apollo/client.ts @@ -1,3 +1,4 @@ +import { getRetryLink } from 'appGraphql/data/apollo/retryLink' import { ApolloClient, from, HttpLink } from '@apollo/client' import { setupSharedApolloCache } from 'uniswap/src/data/cache' import { getDatadogApolloLink } from 'utilities/src/logger/datadog/datadogLink' @@ -9,10 +10,11 @@ if (!API_URL) { const httpLink = new HttpLink({ uri: API_URL }) const datadogLink = getDatadogApolloLink() +const retryLink = getRetryLink() export const apolloClient = new ApolloClient({ connectToDevTools: true, - link: from([datadogLink, httpLink]), + link: from([datadogLink, retryLink, httpLink]), headers: { 'Content-Type': 'application/json', Origin: 'https://app.uniswap.org', diff --git a/apps/web/src/appGraphql/data/apollo/retryLink.ts b/apps/web/src/appGraphql/data/apollo/retryLink.ts new file mode 100644 index 00000000000..3a3400ba187 --- /dev/null +++ b/apps/web/src/appGraphql/data/apollo/retryLink.ts @@ -0,0 +1,43 @@ +import { RetryLink } from '@apollo/client/link/retry' + +/** + * Operations that should retry on network failure. + * These are queries used by useUpdateManualOutage hooks that power the outage banner. + */ +const RETRY_OPERATIONS = new Set([ + // Transaction queries + 'V4TokenTransactions', + 'V3TokenTransactions', + 'V2TokenTransactions', + 'V4Transactions', + 'V3Transactions', + 'V2Transactions', + // Pool queries + 'TopV4Pools', + 'TopV3Pools', + 'TopV2Pairs', +]) + +/** + * Creates an Apollo RetryLink that retries specific operations on network failure. + * Uses exponential backoff with jitter to avoid thundering herd. + */ +export function getRetryLink(): RetryLink { + return new RetryLink({ + delay: { + initial: 1000, + max: 10000, + jitter: true, + }, + attempts: { + max: 3, + retryIf: (error, operation) => { + if (!RETRY_OPERATIONS.has(operation.operationName)) { + return false + } + // Only retry on network errors, not GraphQL errors (validation, auth, etc.) + return !!error?.networkError + }, + }, + }) +} diff --git a/apps/web/src/appGraphql/data/pools/usePoolData.test.ts b/apps/web/src/appGraphql/data/pools/usePoolData.test.ts new file mode 100644 index 00000000000..5c3832377af --- /dev/null +++ b/apps/web/src/appGraphql/data/pools/usePoolData.test.ts @@ -0,0 +1,266 @@ +import { usePoolData } from 'appGraphql/data/pools/usePoolData' +import { FeeAmount } from '@uniswap/v3-sdk' +import { GraphQLApi } from '@universe/api' +import { validBEPoolToken0, validBEPoolToken1 } from 'test-utils/pools/fixtures' +import { renderHook } from 'test-utils/render' +import { V2_DEFAULT_FEE_TIER } from 'uniswap/src/constants/pools' +import { GQL_MAINNET_CHAINS } from 'uniswap/src/features/chains/chainInfo' +import { UniverseChainId } from 'uniswap/src/features/chains/types' + +const { mockV4Query, mockV3Query, mockV2Query, mockUseEnabledChains } = vi.hoisted(() => { + const mockV4Query = vi.fn() + const mockV3Query = vi.fn() + const mockV2Query = vi.fn() + const mockUseEnabledChains = vi.fn() + return { mockV4Query, mockV3Query, mockV2Query, mockUseEnabledChains } +}) + +vi.mock('@universe/api', async () => { + const actual = await vi.importActual('@universe/api') + return { + ...actual, + GraphQLApi: { + ...(actual.GraphQLApi || {}), + useV4PoolQuery: mockV4Query, + useV3PoolQuery: mockV3Query, + useV2PairQuery: mockV2Query, + }, + } +}) + +vi.mock('uniswap/src/features/chains/hooks/useEnabledChains', async () => { + const actual = await vi.importActual('uniswap/src/features/chains/hooks/useEnabledChains') + return { + ...actual, + useEnabledChains: mockUseEnabledChains, + } +}) + +const mockV4PoolData = { + v4Pool: { + poolId: '0xv4pool123', + protocolVersion: GraphQLApi.ProtocolVersion.V4, + feeTier: 500, + isDynamicFee: false, + tickSpacing: 55, + hook: { + id: '0xhook123', + address: '0xhook123', + }, + rewardsCampaign: { + id: 'campaign1', + boostedApr: 5.5, + startTimestamp: 1234567890, + endTimestamp: 1234567990, + }, + token0: validBEPoolToken0, + token0Supply: 100000, + token1: validBEPoolToken1, + token1Supply: 50000, + txCount: 1000, + volume24h: { + value: 500000, + }, + historicalVolume: [ + { value: 100000, timestamp: Date.now() / 1000 - 3600 }, + { value: 200000, timestamp: Date.now() / 1000 - 7200 }, + ], + totalLiquidity: { + value: 1000000, + }, + totalLiquidityPercentChange24h: { + value: 2.5, + }, + }, +} + +const mockV3PoolData = { + v3Pool: { + id: '0xv3pool456', + protocolVersion: GraphQLApi.ProtocolVersion.V3, + address: '0xv3pool456', + feeTier: FeeAmount.MEDIUM, + token0: validBEPoolToken0, + token0Supply: 80000, + token1: validBEPoolToken1, + token1Supply: 40000, + txCount: 800, + volume24h: { + value: 400000, + }, + historicalVolume: [ + { value: 80000, timestamp: Date.now() / 1000 - 3600 }, + { value: 160000, timestamp: Date.now() / 1000 - 7200 }, + ], + totalLiquidity: { + value: 800000, + }, + totalLiquidityPercentChange24h: { + value: 1.8, + }, + }, +} + +const mockV2PairData = { + v2Pair: { + id: '0xv2pair789', + protocolVersion: GraphQLApi.ProtocolVersion.V2, + address: '0xv2pair789', + token0: validBEPoolToken0, + token0Supply: 60000, + token1: validBEPoolToken1, + token1Supply: 30000, + txCount: 600, + volume24h: { + value: 300000, + }, + historicalVolume: [ + { value: 60000, timestamp: Date.now() / 1000 - 3600 }, + { value: 120000, timestamp: Date.now() / 1000 - 7200 }, + ], + totalLiquidity: { + value: 600000, + }, + totalLiquidityPercentChange24h: { + value: 1.2, + }, + }, +} + +describe('usePoolData', () => { + beforeEach(() => { + vi.clearAllMocks() + + mockUseEnabledChains.mockReturnValue({ + isTestnetModeEnabled: false, + defaultChainId: UniverseChainId.Mainnet, + chains: [UniverseChainId.Mainnet], + gqlChains: GQL_MAINNET_CHAINS, + }) + + mockV4Query.mockReturnValue({ loading: false, error: undefined, data: undefined }) + mockV3Query.mockReturnValue({ loading: false, error: undefined, data: undefined }) + mockV2Query.mockReturnValue({ loading: false, error: undefined, data: undefined }) + }) + + it('should return v4 pool data with hook address and rewards campaign', () => { + mockV4Query.mockReturnValue({ + loading: false, + error: undefined, + data: mockV4PoolData, + }) + + const { result } = renderHook(() => + usePoolData({ + poolIdOrAddress: '0xv4pool123', + chainId: UniverseChainId.Mainnet, + isPoolAddress: false, + }), + ) + + expect(result.current.loading).toBe(false) + expect(result.current.error).toBe(false) + expect(result.current.data).toBeDefined() + + expect(result.current.data?.idOrAddress).toBe('0xv4pool123') + expect(result.current.data?.protocolVersion).toBe(GraphQLApi.ProtocolVersion.V4) + expect(result.current.data?.feeTier?.feeAmount).toBe(500) + expect(result.current.data?.feeTier?.tickSpacing).toBe(55) + expect(result.current.data?.feeTier?.isDynamic).toBe(false) + expect(result.current.data?.hookAddress).toBe('0xhook123') + expect(result.current.data?.rewardsCampaign).toEqual({ + id: 'campaign1', + boostedApr: 5.5, + startTimestamp: 1234567890, + endTimestamp: 1234567990, + }) + + expect(result.current.data?.token0).toEqual(validBEPoolToken0) + expect(result.current.data?.token1).toEqual(validBEPoolToken1) + expect(result.current.data?.tvlToken0).toBe(100000) + expect(result.current.data?.tvlToken1).toBe(50000) + + expect(result.current.data?.volumeUSD24H).toBe(500000) + expect(result.current.data?.tvlUSD).toBe(1000000) + expect(result.current.data?.tvlUSDChange).toBe(2.5) + expect(result.current.data?.txCount).toBe(1000) + }) + + it('should return v3 pool data with calculated fee tier', () => { + mockV3Query.mockReturnValue({ + loading: false, + error: undefined, + data: mockV3PoolData, + }) + + const { result } = renderHook(() => + usePoolData({ + poolIdOrAddress: '0xv3pool456', + chainId: UniverseChainId.Mainnet, + isPoolAddress: true, + }), + ) + + expect(result.current.loading).toBe(false) + expect(result.current.error).toBe(false) + expect(result.current.data).toBeDefined() + + expect(result.current.data?.idOrAddress).toBe('0xv3pool456') + expect(result.current.data?.protocolVersion).toBe(GraphQLApi.ProtocolVersion.V3) + expect(result.current.data?.feeTier?.feeAmount).toBe(FeeAmount.MEDIUM) + expect(result.current.data?.feeTier?.tickSpacing).toBe(60) + expect(result.current.data?.feeTier?.isDynamic).toBe(false) + + expect(result.current.data?.hookAddress).toBeUndefined() + expect(result.current.data?.rewardsCampaign).toBeUndefined() + + expect(result.current.data?.token0).toEqual(validBEPoolToken0) + expect(result.current.data?.token1).toEqual(validBEPoolToken1) + expect(result.current.data?.tvlToken0).toBe(80000) + expect(result.current.data?.tvlToken1).toBe(40000) + + expect(result.current.data?.volumeUSD24H).toBe(400000) + expect(result.current.data?.tvlUSD).toBe(800000) + expect(result.current.data?.tvlUSDChange).toBe(1.8) + expect(result.current.data?.txCount).toBe(800) + }) + + it('should return v2 pool data with default fee tier', () => { + mockV2Query.mockReturnValue({ + loading: false, + error: undefined, + data: mockV2PairData, + }) + + const { result } = renderHook(() => + usePoolData({ + poolIdOrAddress: '0xv2pair789', + chainId: UniverseChainId.Mainnet, + isPoolAddress: true, + }), + ) + + expect(result.current.loading).toBe(false) + expect(result.current.error).toBe(false) + expect(result.current.data).toBeDefined() + + expect(result.current.data?.idOrAddress).toBe('0xv2pair789') + expect(result.current.data?.protocolVersion).toBe(GraphQLApi.ProtocolVersion.V2) + expect(result.current.data?.feeTier?.feeAmount).toBe(V2_DEFAULT_FEE_TIER) + expect(result.current.data?.feeTier?.tickSpacing).toBeUndefined() + expect(result.current.data?.feeTier?.isDynamic).toBe(false) + + expect(result.current.data?.hookAddress).toBeUndefined() + expect(result.current.data?.rewardsCampaign).toBeUndefined() + + expect(result.current.data?.token0).toEqual(validBEPoolToken0) + expect(result.current.data?.token1).toEqual(validBEPoolToken1) + expect(result.current.data?.tvlToken0).toBe(60000) + expect(result.current.data?.tvlToken1).toBe(30000) + + expect(result.current.data?.volumeUSD24H).toBe(300000) + expect(result.current.data?.tvlUSD).toBe(600000) + expect(result.current.data?.tvlUSDChange).toBe(1.2) + expect(result.current.data?.txCount).toBe(600) + }) +}) diff --git a/apps/web/src/appGraphql/data/pools/usePoolData.ts b/apps/web/src/appGraphql/data/pools/usePoolData.ts index e2d1bf91be7..cedee21df58 100644 --- a/apps/web/src/appGraphql/data/pools/usePoolData.ts +++ b/apps/web/src/appGraphql/data/pools/usePoolData.ts @@ -1,8 +1,9 @@ +import { FeeAmount, TICK_SPACINGS } from '@uniswap/v3-sdk' import { GraphQLApi } from '@universe/api' import { FeeData } from 'components/Liquidity/Create/types' import ms from 'ms' import { useMemo } from 'react' -import { DEFAULT_TICK_SPACING, V2_DEFAULT_FEE_TIER } from 'uniswap/src/constants/pools' +import { V2_DEFAULT_FEE_TIER } from 'uniswap/src/constants/pools' import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { toGraphQLChain } from 'uniswap/src/features/chains/utils' @@ -130,7 +131,7 @@ export function usePoolData({ const pool = dataV4?.v4Pool ?? dataV3?.v3Pool ?? dataV2?.v2Pair ?? undefined const feeTier: FeeData = { feeAmount: dataV4?.v4Pool?.feeTier ?? dataV3?.v3Pool?.feeTier ?? V2_DEFAULT_FEE_TIER, - tickSpacing: DEFAULT_TICK_SPACING, + tickSpacing: dataV4?.v4Pool?.tickSpacing ?? TICK_SPACINGS[dataV3?.v3Pool?.feeTier as FeeAmount], isDynamic: dataV4?.v4Pool?.isDynamicFee ?? false, } const poolId = dataV4?.v4Pool?.poolId ?? dataV3?.v3Pool?.address ?? dataV2?.v2Pair?.address ?? poolIdOrAddress diff --git a/apps/web/src/assets/images/portfolio-connect-wallet-banner-grid/dark.svg b/apps/web/src/assets/images/portfolio-connect-wallet-banner-grid/dark.svg new file mode 100644 index 00000000000..58541a4f109 --- /dev/null +++ b/apps/web/src/assets/images/portfolio-connect-wallet-banner-grid/dark.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/src/assets/images/portfolio-connect-wallet-banner-grid/light.svg b/apps/web/src/assets/images/portfolio-connect-wallet-banner-grid/light.svg new file mode 100644 index 00000000000..4a2adf55f55 --- /dev/null +++ b/apps/web/src/assets/images/portfolio-connect-wallet-banner-grid/light.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/src/assets/images/portfolio-page-disconnected-preview/dark.svg b/apps/web/src/assets/images/portfolio-page-disconnected-preview/dark.svg new file mode 100644 index 00000000000..1ae6e3cb76e --- /dev/null +++ b/apps/web/src/assets/images/portfolio-page-disconnected-preview/dark.svg @@ -0,0 +1,778 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/src/assets/images/portfolio-page-disconnected-preview/light.svg b/apps/web/src/assets/images/portfolio-page-disconnected-preview/light.svg new file mode 100644 index 00000000000..8e9c0579fe4 --- /dev/null +++ b/apps/web/src/assets/images/portfolio-page-disconnected-preview/light.svg @@ -0,0 +1,778 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/src/assets/images/portfolio-page-disconnected-preview/mobile-dark.svg b/apps/web/src/assets/images/portfolio-page-disconnected-preview/mobile-dark.svg new file mode 100644 index 00000000000..843ed723d5d --- /dev/null +++ b/apps/web/src/assets/images/portfolio-page-disconnected-preview/mobile-dark.svg @@ -0,0 +1,189 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/src/assets/images/portfolio-page-disconnected-preview/mobile-light.svg b/apps/web/src/assets/images/portfolio-page-disconnected-preview/mobile-light.svg new file mode 100644 index 00000000000..9530c38d4e9 --- /dev/null +++ b/apps/web/src/assets/images/portfolio-page-disconnected-preview/mobile-light.svg @@ -0,0 +1,189 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/src/assets/svg/demo-wallet-emblem.svg b/apps/web/src/assets/svg/demo-wallet-emblem.svg new file mode 100644 index 00000000000..1839d363511 --- /dev/null +++ b/apps/web/src/assets/svg/demo-wallet-emblem.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/src/components/AccountDetails/AddressDisplay.tsx b/apps/web/src/components/AccountDetails/AddressDisplay.tsx index 56d5bc9eb1d..ef1e13367ba 100644 --- a/apps/web/src/components/AccountDetails/AddressDisplay.tsx +++ b/apps/web/src/components/AccountDetails/AddressDisplay.tsx @@ -1,4 +1,4 @@ -import styled from 'lib/styled-components' +import { deprecatedStyled } from 'lib/styled-components' import { EllipsisStyle } from 'theme/components/styles' import { Flex } from 'ui/src' import { Unitag } from 'ui/src/components/icons/Unitag' @@ -7,7 +7,7 @@ import { useENSName } from 'uniswap/src/features/ens/api' import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { shortenAddress } from 'utilities/src/addresses' -const IdentifierText = styled.span` +const IdentifierText = deprecatedStyled.span` ${EllipsisStyle} ` diff --git a/apps/web/src/components/AccountDetails/MultiBlockchainAddressDisplay.tsx b/apps/web/src/components/AccountDetails/MultiBlockchainAddressDisplay.tsx index 33b601988f3..895deef738f 100644 --- a/apps/web/src/components/AccountDetails/MultiBlockchainAddressDisplay.tsx +++ b/apps/web/src/components/AccountDetails/MultiBlockchainAddressDisplay.tsx @@ -49,11 +49,13 @@ function PrimaryAddressDisplay({ ensName, primaryAddress, isMultipleAddresses, + hideAddressInSubtitle, }: { unitag?: string ensName?: string primaryAddress: string isMultipleAddresses: boolean + hideAddressInSubtitle?: boolean }) { const { t } = useTranslation() const shortenedPrimaryAddress = shortenAddress({ address: primaryAddress }) @@ -62,17 +64,18 @@ function PrimaryAddressDisplay({ return ( - {isMultipleAddresses ? ( - - {shortenedPrimaryAddress} {t('common.plusMore', { number: 1 })} - - ) : ( - + {!hideAddressInSubtitle && + (isMultipleAddresses ? ( - {shortenedPrimaryAddress} + {shortenedPrimaryAddress} {t('common.plusMore', { number: 1 })} - - )} + ) : ( + + + {shortenedPrimaryAddress} + + + ))} ) } @@ -138,7 +141,7 @@ function TooltipAccountRow({ account }: { account: AccountItem }) { ) } -export function MultiBlockchainAddressDisplay() { +export function MultiBlockchainAddressDisplay({ hideAddressInSubtitle }: { hideAddressInSubtitle?: boolean }) { const activeAddresses = useActiveAddresses() const evmAddress = activeAddresses.evmAddress const { data: ensName } = useENSName(evmAddress) @@ -193,6 +196,7 @@ export function MultiBlockchainAddressDisplay() { ensName={ensName ?? undefined} primaryAddress={primaryAddress} isMultipleAddresses={isMultipleAddresses} + hideAddressInSubtitle={hideAddressInSubtitle} /> } /> diff --git a/apps/web/src/components/AccountDrawer/AccountDrawer.e2e.test.ts b/apps/web/src/components/AccountDrawer/AccountDrawer.e2e.test.ts index 3d4eab9b0d6..d6a01855fa7 100644 --- a/apps/web/src/components/AccountDrawer/AccountDrawer.e2e.test.ts +++ b/apps/web/src/components/AccountDrawer/AccountDrawer.e2e.test.ts @@ -26,140 +26,155 @@ async function countPortfolioBalancesQueries(page: Page, actions: () => Promise< return portfolioBalanceCount } -test.describe('Mini Portfolio settings', () => { - test.beforeEach(async ({ page }) => { - await page.goto('/swap') - await page.getByTestId(TestID.Web3StatusConnected).click() - await page.getByTestId(TestID.WalletSettings).click() - }) - test('changes theme', async ({ page }) => { - await page.getByTestId(TestID.ThemeDark).click() - await expect(page.locator('html')).toHaveClass('t_dark') - await page.getByTestId(TestID.ThemeLight).click() - await expect(page.locator('html')).toHaveClass('t_light') - }) - - test('changes language', async ({ page }) => { - await page.getByTestId(TestID.LanguageSettingsButton).click() - await page.getByRole('link', { name: 'Spanish (Spain)' }).click() - await expect(page.getByText('Uniswap está disponible en:')).toBeVisible() - await page.reload() - await expect(page.url()).toContain('lng=es-ES') - await expect(page.getByText('Uniswap está disponible en:')).toBeVisible() - }) - - test('toggles testnet', async ({ page }) => { - await page.getByTestId(TestID.TestnetsToggle).click() - await expect(page.getByTestId(TestID.TestnetsToggle)).toHaveAttribute('aria-checked', 'true') - await expect(page.getByText('Swapping on Sepolia')).toBeVisible() - }) - - test('disconnected wallet settings should not be accessible', async ({ page }) => { - await page.goto('/swap?eagerlyConnect=false') - await page.getByLabel('Navigation button').click() - await expect(page.getByTestId(TestID.WalletSettings)).not.toBeVisible() - }) - - test('settings on mobile should be accessible via bottom sheet', async ({ page }) => { - await page.setViewportSize({ width: 375, height: 667 }) - await expect(page.getByTestId(TestID.AccountDrawer).first()).toHaveAttribute('class', /is_Sheet/) - }) -}) - -test.describe('Mini Portfolio account drawer', () => { - test.beforeEach(async ({ page, graphql }) => { - // Set up request interception for portfolio balances - await graphql.intercept('PortfolioBalances', Mocks.PortfolioBalances.hayden) - await graphql.intercept('NftsTab', Mocks.Account.nfts) - await graphql.intercept('ActivityWeb', Mocks.Account.full_activity_history) - await page.goto(`/swap?eagerlyConnectAddress=${HAYDEN_ADDRESS}`) - }) - - test('should fetch balances when the account drawer is opened', async ({ page }) => { - const portfolioBalanceCount = await countPortfolioBalancesQueries(page, async () => { - // Click to open drawer - await page.getByTestId(TestID.Web3StatusConnected).click() - await expect(page.getByTestId(TestID.AccountDrawer)).toBeVisible() - }) - - expect(portfolioBalanceCount).toBe(1) - }) - - test('should not re-fetch balances on second open', async ({ page }) => { - // First, open drawer and let it fetch data (this should trigger a request) - await page.getByTestId(TestID.Web3StatusConnected).click() - await expect(page.getByTestId(TestID.AccountDrawer)).toBeVisible() - - // Wait for the portfolio data to actually load - await page.getByTestId(TestID.MiniPortfolioPage).waitFor() - - // Close the drawer - await page.getByTestId(TestID.CloseAccountDrawer).click() - await expect(page.getByTestId(TestID.AccountDrawer)).not.toBeVisible() - - // Now test opening it a second time (should not trigger another request due to caching) - const portfolioBalanceCount = await countPortfolioBalancesQueries(page, async () => { - // Click to open drawer again - await page.getByTestId(TestID.Web3StatusConnected).click() - await expect(page.getByTestId(TestID.AccountDrawer)).toBeVisible() +test.describe( + 'Account Drawer', + { + tag: '@team:apps-portfolio', + annotation: [ + { type: 'DD_TAGS[team]', description: 'apps-portfolio' }, + { type: 'DD_TAGS[test.type]', description: 'web-e2e' }, + ], + }, + () => { + test.describe('Mini Portfolio settings', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/swap') + await page.getByTestId(TestID.Web3StatusConnected).click() + await page.getByTestId(TestID.WalletSettings).click() + }) + test('changes theme', async ({ page }) => { + await page.getByTestId(TestID.ThemeDark).click() + await expect(page.locator('html')).toHaveClass('t_dark') + await page.getByTestId(TestID.ThemeLight).click() + await expect(page.locator('html')).toHaveClass('t_light') + }) + + test('changes language', async ({ page }) => { + await page.getByTestId(TestID.LanguageSettingsButton).click() + await page.getByRole('link', { name: 'Spanish (Spain)' }).click() + await expect(page.getByText('Uniswap está disponible en:')).toBeVisible() + await page.reload() + await expect(page.url()).toContain('lng=es-ES') + await expect(page.getByText('Uniswap está disponible en:')).toBeVisible() + }) + + test('toggles testnet', async ({ page }) => { + await page.getByTestId(TestID.TestnetsToggle).click() + await expect(page.getByTestId(TestID.TestnetsToggle)).toHaveAttribute('aria-checked', 'true') + // Confirm the info modal appears and then close it + const modalButton = page.getByRole('button', { name: 'Close' }) + await expect(modalButton).toBeVisible() + await modalButton.click() + }) + + test('disconnected wallet settings should not be accessible', async ({ page }) => { + await page.goto('/swap?eagerlyConnect=false') + await page.getByLabel('Navigation button').click() + await expect(page.getByTestId(TestID.WalletSettings)).not.toBeVisible() + }) + + test('settings on mobile should be accessible via bottom sheet', async ({ page }) => { + await page.setViewportSize({ width: 375, height: 667 }) + await expect(page.getByTestId(TestID.AccountDrawer).first()).toHaveAttribute('class', /is_Sheet/) + }) }) - expect(portfolioBalanceCount).toBe(0) - }) - - test('fetches account information', async ({ page }) => { - // Open the mini portfolio - await page.getByTestId(TestID.Web3StatusConnected).click() - - // Wait for the drawer and main content to load - await expect(page.getByTestId(TestID.AccountDrawer)).toBeVisible() - await page.getByTestId(TestID.MiniPortfolioPage).waitFor() - - // Verify wallet state - wait for tokens tab to load - await expect(page.getByTestId(TestID.MiniPortfolioNavbar)).toContainText('Tokens') - await expect(page.getByTestId(TestID.MiniPortfolioPage)).toContainText('Hidden tokens') - - // Check NFTs section - await page.getByTestId(TestID.MiniPortfolioNavbar).getByText('NFTs').click() - await page.waitForTimeout(15_000) - await expect( - page.getByTestId(`${TestID.MiniPortfolioNftItem}-${'0x3C90502f0CB0ad0A48c51357E65Ff15247A1D88E'}-${21}`), - ).toBeVisible() - - // Check Activity section - await page.getByTestId(TestID.MiniPortfolioNavbar).getByText('Activity').click() - await expect(page.getByTestId(TestID.MiniPortfolioPage)).toContainText('Contract Interaction') - }) - - test('refetches balances when account changes', async ({ page, graphql }) => { - // Open account drawer with first account - await page.getByTestId(TestID.Web3StatusConnected).click() - const drawer = page.getByTestId(TestID.AccountDrawer) - await expect(drawer).toBeVisible() - await page.getByTestId(TestID.MiniPortfolioPage).waitFor() - - // Verify first account address - await expect(drawer.getByText(HAYDEN_ADDRESS.slice(0, 6))).toBeVisible() - - // Set up mock data for second account - await graphql.intercept('PortfolioBalances', Mocks.PortfolioBalances.test_wallet) - - // Count portfolio requests triggered by account change - const portfolioRequestCount = await countPortfolioBalancesQueries(page, async () => { - // Switch to second account (this should trigger new portfolio requests) - await page.goto(`/swap`) - - // Open drawer with new account - await page.getByTestId(TestID.Web3StatusConnected).click() - const newDrawer = page.getByTestId(TestID.AccountDrawer) - await expect(newDrawer).toBeVisible() - await page.getByTestId(TestID.MiniPortfolioPage).waitFor() - - // Verify new account address - await expect(newDrawer.getByText('test0')).toBeVisible() + test.describe('Mini Portfolio account drawer', () => { + test.beforeEach(async ({ page, graphql }) => { + // Set up request interception for portfolio balances + await graphql.intercept('PortfolioBalances', Mocks.PortfolioBalances.hayden) + await graphql.intercept('NftsTab', Mocks.Account.nfts) + await graphql.intercept('ActivityWeb', Mocks.Account.full_activity_history) + await page.goto(`/swap?eagerlyConnectAddress=${HAYDEN_ADDRESS}`) + }) + + test('should fetch balances when the account drawer is opened', async ({ page }) => { + const portfolioBalanceCount = await countPortfolioBalancesQueries(page, async () => { + // Click to open drawer + await page.getByTestId(TestID.Web3StatusConnected).click() + await expect(page.getByTestId(TestID.AccountDrawer)).toBeVisible() + }) + + expect(portfolioBalanceCount).toBe(1) + }) + + test('should not re-fetch balances on second open', async ({ page }) => { + // First, open drawer and let it fetch data (this should trigger a request) + await page.getByTestId(TestID.Web3StatusConnected).click() + await expect(page.getByTestId(TestID.AccountDrawer)).toBeVisible() + + // Wait for the portfolio data to actually load + await page.getByTestId(TestID.MiniPortfolioPage).waitFor() + + // Close the drawer + await page.getByTestId(TestID.CloseAccountDrawer).click() + await expect(page.getByTestId(TestID.AccountDrawer)).not.toBeVisible() + + // Now test opening it a second time (should not trigger another request due to caching) + const portfolioBalanceCount = await countPortfolioBalancesQueries(page, async () => { + // Click to open drawer again + await page.getByTestId(TestID.Web3StatusConnected).click() + await expect(page.getByTestId(TestID.AccountDrawer)).toBeVisible() + }) + + expect(portfolioBalanceCount).toBe(0) + }) + + test('fetches account information', async ({ page }) => { + // Open the mini portfolio + await page.getByTestId(TestID.Web3StatusConnected).click() + + // Wait for the drawer and main content to load + await expect(page.getByTestId(TestID.AccountDrawer)).toBeVisible() + await page.getByTestId(TestID.MiniPortfolioPage).waitFor() + + // Verify wallet state - wait for tokens tab to load + await expect(page.getByTestId(TestID.MiniPortfolioNavbar)).toContainText('Tokens') + await expect(page.getByTestId(TestID.MiniPortfolioPage)).toContainText('Hidden tokens') + + // Check NFTs section + await page.getByTestId(TestID.MiniPortfolioNavbar).getByText('NFTs').click() + await page.waitForTimeout(15_000) + await expect( + page.getByTestId(`${TestID.MiniPortfolioNftItem}-${'0x3C90502f0CB0ad0A48c51357E65Ff15247A1D88E'}-${21}`), + ).toBeVisible() + + // Check Activity section + await page.getByTestId(TestID.MiniPortfolioNavbar).getByText('Activity').click() + await expect(page.getByTestId(TestID.MiniPortfolioPage)).toContainText('Contract Interaction') + }) + + test('refetches balances when account changes', async ({ page, graphql }) => { + // Open account drawer with first account + await page.getByTestId(TestID.Web3StatusConnected).click() + const drawer = page.getByTestId(TestID.AccountDrawer) + await expect(drawer).toBeVisible() + await page.getByTestId(TestID.MiniPortfolioPage).waitFor() + + // Verify first account address + await expect(drawer.getByText(HAYDEN_ADDRESS.slice(0, 6))).toBeVisible() + + // Set up mock data for second account + await graphql.intercept('PortfolioBalances', Mocks.PortfolioBalances.test_wallet) + + // Count portfolio requests triggered by account change + const portfolioRequestCount = await countPortfolioBalancesQueries(page, async () => { + // Switch to second account (this should trigger new portfolio requests) + await page.goto(`/swap`) + + // Open drawer with new account + await page.getByTestId(TestID.Web3StatusConnected).click() + const newDrawer = page.getByTestId(TestID.AccountDrawer) + await expect(newDrawer).toBeVisible() + await page.getByTestId(TestID.MiniPortfolioPage).waitFor() + + // Verify new account address + await expect(newDrawer.getByText('test0')).toBeVisible() + }) + + // Verify that account change triggered portfolio requests + expect(portfolioRequestCount).toBeGreaterThanOrEqual(1) + }) }) - - // Verify that account change triggered portfolio requests - expect(portfolioRequestCount).toBeGreaterThanOrEqual(1) - }) -}) + }, +) diff --git a/apps/web/src/components/AccountDrawer/ActionTile.tsx b/apps/web/src/components/AccountDrawer/ActionTile.tsx index cabbd3f44a3..f0286745cc1 100644 --- a/apps/web/src/components/AccountDrawer/ActionTile.tsx +++ b/apps/web/src/components/AccountDrawer/ActionTile.tsx @@ -1,6 +1,6 @@ import { ReactNode } from 'react' import { SpinnerSVG } from 'theme/components/icons/spinner' -import { Flex, styled, Text, useSporeColors } from 'ui/src' +import { Flex, FlexProps, styled, Text, useSporeColors } from 'ui/src' const LoadingButtonSpinner = (props: React.ComponentPropsWithoutRef<'svg'>) => ( @@ -39,6 +39,16 @@ const Tile = styled(Flex, { }, }) +export type ActionTileProps = { + dataTestId: string + Icon: ReactNode + name: string + onClick: () => void + loading?: boolean + disabled?: boolean + padding?: FlexProps['p'] +} + export function ActionTile({ dataTestId, Icon, @@ -46,20 +56,14 @@ export function ActionTile({ onClick, loading, disabled, -}: { - dataTestId: string - Icon: ReactNode - name: string - onClick: () => void - loading?: boolean - disabled?: boolean -}) { + padding = '$spacing12', +}: ActionTileProps) { const { accent1 } = useSporeColors() return ( - + {loading ? : Icon} - + {name} diff --git a/apps/web/src/components/AccountDrawer/AuthenticatedHeader.anvil.e2e.test.ts b/apps/web/src/components/AccountDrawer/AuthenticatedHeader.anvil.e2e.test.ts index 38ef1adba12..36604cfc25b 100644 --- a/apps/web/src/components/AccountDrawer/AuthenticatedHeader.anvil.e2e.test.ts +++ b/apps/web/src/components/AccountDrawer/AuthenticatedHeader.anvil.e2e.test.ts @@ -4,23 +4,33 @@ import { HAYDEN_ADDRESS, HAYDEN_ENS, UNITAG_NAME } from 'playwright/fixtures/wal const test = getTest({ withAnvil: true }) -test.describe('AuthenticatedHeader unitag and ENS display', () => { - // Test cases: - // 1. Shows ENS, followed by address, if ENS exists but not Unitag - // 2. Shows Unitag, followed by address, if user has both Unitag and ENS +test.describe( + 'AuthenticatedHeader unitag and ENS display', + { + tag: '@team:apps-growth', + annotation: [ + { type: 'DD_TAGS[team]', description: 'apps-growth' }, + { type: 'DD_TAGS[test.type]', description: 'web-e2e' }, + ], + }, + () => { + // Test cases: + // 1. Shows ENS, followed by address, if ENS exists but not Unitag + // 2. Shows Unitag, followed by address, if user has both Unitag and ENS - const ACCOUNT_WITH_ENS = HAYDEN_ADDRESS + const ACCOUNT_WITH_ENS = HAYDEN_ADDRESS - test('shows ENS, followed by address when ENS exists but not Unitag', async ({ page }) => { - await page.goto(`/swap?eagerlyConnectAddress=${ACCOUNT_WITH_ENS}`) + test('shows ENS, followed by address when ENS exists but not Unitag', async ({ page }) => { + await page.goto(`/swap?eagerlyConnectAddress=${ACCOUNT_WITH_ENS}`) - await openAccountDrawerAndVerify({ page, expectedPrimaryText: HAYDEN_ENS, walletAddress: HAYDEN_ADDRESS }) - }) + await openAccountDrawerAndVerify({ page, expectedPrimaryText: HAYDEN_ENS, walletAddress: HAYDEN_ADDRESS }) + }) - test('shows Unitag when user has both Unitag and ENS', async ({ page }) => { - await mockUnitagResponse({ page, address: HAYDEN_ADDRESS, unitag: UNITAG_NAME }) - await page.goto(`/swap?eagerlyConnectAddress=${HAYDEN_ADDRESS}`) + test('shows Unitag when user has both Unitag and ENS', async ({ page }) => { + await mockUnitagResponse({ page, address: HAYDEN_ADDRESS, unitag: UNITAG_NAME }) + await page.goto(`/swap?eagerlyConnectAddress=${HAYDEN_ADDRESS}`) - await openAccountDrawerAndVerify({ page, expectedPrimaryText: UNITAG_NAME, walletAddress: HAYDEN_ADDRESS }) - }) -}) + await openAccountDrawerAndVerify({ page, expectedPrimaryText: UNITAG_NAME, walletAddress: HAYDEN_ADDRESS }) + }) + }, +) diff --git a/apps/web/src/components/AccountDrawer/AuthenticatedHeader.e2e.test.ts b/apps/web/src/components/AccountDrawer/AuthenticatedHeader.e2e.test.ts index e1348cbf0fd..d9e57ed8817 100644 --- a/apps/web/src/components/AccountDrawer/AuthenticatedHeader.e2e.test.ts +++ b/apps/web/src/components/AccountDrawer/AuthenticatedHeader.e2e.test.ts @@ -4,22 +4,32 @@ import { TEST_WALLET_ADDRESS, UNITAG_NAME } from 'playwright/fixtures/wallets' const test = getTest() -test.describe('AuthenticatedHeader unitag and ENS display', () => { - // Test cases: - // 1. Shows address if no Unitag or ENS exists - // 2. Shows Unitag, followed by address, if Unitag exists but not ENS +test.describe( + 'AuthenticatedHeader unitag and ENS display', + { + tag: '@team:apps-growth', + annotation: [ + { type: 'DD_TAGS[team]', description: 'apps-growth' }, + { type: 'DD_TAGS[test.type]', description: 'web-e2e' }, + ], + }, + () => { + // Test cases: + // 1. Shows address if no Unitag or ENS exists + // 2. Shows Unitag, followed by address, if Unitag exists but not ENS - const ACCOUNT_WITH_NO_USERNAME = '0xF030EaA01aFf57A23483dC8A1c3550d153be69Fb' + const ACCOUNT_WITH_NO_USERNAME = '0xF030EaA01aFf57A23483dC8A1c3550d153be69Fb' - test('shows address if no Unitag or ENS exists', async ({ page }) => { - await page.goto(`/swap?eagerlyConnectAddress=${ACCOUNT_WITH_NO_USERNAME}`) - await openAccountDrawerAndVerify({ page, walletAddress: ACCOUNT_WITH_NO_USERNAME }) - }) + test('shows address if no Unitag or ENS exists', async ({ page }) => { + await page.goto(`/swap?eagerlyConnectAddress=${ACCOUNT_WITH_NO_USERNAME}`) + await openAccountDrawerAndVerify({ page, walletAddress: ACCOUNT_WITH_NO_USERNAME }) + }) - test('shows Unitag, followed by address when Unitag exists but not ENS', async ({ page }) => { - await mockUnitagResponse({ page, address: TEST_WALLET_ADDRESS, unitag: UNITAG_NAME }) - await page.goto('/swap') + test('shows Unitag, followed by address when Unitag exists but not ENS', async ({ page }) => { + await mockUnitagResponse({ page, address: TEST_WALLET_ADDRESS, unitag: UNITAG_NAME }) + await page.goto('/swap') - await openAccountDrawerAndVerify({ page, expectedPrimaryText: UNITAG_NAME, walletAddress: TEST_WALLET_ADDRESS }) - }) -}) + await openAccountDrawerAndVerify({ page, expectedPrimaryText: UNITAG_NAME, walletAddress: TEST_WALLET_ADDRESS }) + }) + }, +) diff --git a/apps/web/src/components/AccountDrawer/AuthenticatedHeader.tsx b/apps/web/src/components/AccountDrawer/AuthenticatedHeader.tsx index 72bdd2ef2ee..a17f7e3d04a 100644 --- a/apps/web/src/components/AccountDrawer/AuthenticatedHeader.tsx +++ b/apps/web/src/components/AccountDrawer/AuthenticatedHeader.tsx @@ -2,40 +2,37 @@ import { NetworkStatus } from '@apollo/client' import { CurrencyAmount, Token } from '@uniswap/sdk-core' import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { MultiBlockchainAddressDisplay } from 'components/AccountDetails/MultiBlockchainAddressDisplay' -import { ActionTile } from 'components/AccountDrawer/ActionTile' import { DisconnectButton } from 'components/AccountDrawer/DisconnectButton' import { DownloadGraduatedWalletCard } from 'components/AccountDrawer/DownloadGraduatedWalletCard' import { EmptyWallet } from 'components/AccountDrawer/MiniPortfolio/EmptyWallet' import { ExtensionDeeplinks } from 'components/AccountDrawer/MiniPortfolio/ExtensionDeeplinks' import { useAccountDrawer } from 'components/AccountDrawer/MiniPortfolio/hooks' import MiniPortfolio from 'components/AccountDrawer/MiniPortfolio/MiniPortfolio' -import { SendButtonTooltip } from 'components/AccountDrawer/SendButtonTooltip' +import MiniPortfolioV2 from 'components/AccountDrawer/MiniPortfolio/MiniPortfolioV2' +import { ReceiveActionTile } from 'components/ActionTiles/ReceiveActionTile' +import { SendActionTile } from 'components/ActionTiles/SendActionTile/SendActionTile' import { LimitedSupportBanner } from 'components/Banner/LimitedSupportBanner' import DelegationMismatchModal from 'components/delegation/DelegationMismatchModal' import { Settings } from 'components/Icons/Settings' -import { ReceiveModalState } from 'components/ReceiveCryptoModal/types' -import { useOpenReceiveCryptoModal } from 'components/ReceiveCryptoModal/useOpenReceiveCryptoModal' import StatusIcon from 'components/StatusIcon' +import { ExtensionRequestMethods, useUniswapExtensionRequest } from 'components/WalletModal/useWagmiConnectorWithId' import { useAccountsStore } from 'features/accounts/store/hooks' import { useIsUniswapExtensionConnected } from 'hooks/useIsUniswapExtensionConnected' import { useModalState } from 'hooks/useModalState' -import { useTheme } from 'lib/styled-components' -import { useState } from 'react' -import { Trans, useTranslation } from 'react-i18next' +import { useCallback, useState } from 'react' +import { useTranslation } from 'react-i18next' import { useUserHasAvailableClaim, useUserUnclaimedAmount } from 'state/claim/hooks' -import { Button, Flex, IconButton } from 'ui/src' -import { ArrowDownCircleFilled } from 'ui/src/components/icons/ArrowDownCircleFilled' -import { SendAction } from 'ui/src/components/icons/SendAction' +import { Button, Flex, IconButton, Image, useSporeColors } from 'ui/src' +import { UNISWAP_LOGO } from 'ui/src/assets' import { Shine } from 'ui/src/loading/Shine' +import { iconSizes } from 'ui/src/theme' import AnimatedNumber, { BALANCE_CHANGE_INDICATION_DURATION, } from 'uniswap/src/components/AnimatedNumber/AnimatedNumber' import { TestnetModeBanner } from 'uniswap/src/components/banners/TestnetModeBanner' import { RelativeChange } from 'uniswap/src/components/RelativeChange/RelativeChange' -import { useUniswapContext } from 'uniswap/src/contexts/UniswapContext' import { useConnectionStatus } from 'uniswap/src/features/accounts/store/hooks' import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' -import { UniverseChainId } from 'uniswap/src/features/chains/types' import { usePortfolioTotalValue } from 'uniswap/src/features/dataApi/balances/balancesRest' import { FiatCurrency } from 'uniswap/src/features/fiatCurrency/constants' import { useAppFiatCurrency, useAppFiatCurrencyInfo } from 'uniswap/src/features/fiatCurrency/hooks' @@ -46,7 +43,6 @@ import { ModalName } from 'uniswap/src/features/telemetry/constants' import i18next from 'uniswap/src/i18n' import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { NumberType } from 'utilities/src/format/types' -import { useEvent } from 'utilities/src/react/hooks' export default function AuthenticatedHeader({ evmAddress, @@ -58,6 +54,7 @@ export default function AuthenticatedHeader({ openSettings: () => void }) { const { t } = useTranslation() + const isPortfolioPageEnabled = useFeatureFlag(FeatureFlags.PortfolioPage) const isSolanaConnected = useConnectionStatus(Platform.SVM).isConnected const multipleWalletsConnected = useAccountsStore((state) => { @@ -66,7 +63,10 @@ export default function AuthenticatedHeader({ return Boolean(evmWalletId && svmWalletId && evmWalletId !== svmWalletId) }) // if different wallets are connected, do not show mini wallet icon - const shouldShowExtensionDeeplinks = useIsUniswapExtensionConnected() && !isSolanaConnected + const isUniswapExtensionConnected = useIsUniswapExtensionConnected() + const uniswapExtensionRequest = useUniswapExtensionRequest() + const shouldShowExtensionDeeplinks = isUniswapExtensionConnected && !isSolanaConnected && !isPortfolioPageEnabled + const shouldShowExtensionButton = isPortfolioPageEnabled && isUniswapExtensionConnected && !isSolanaConnected const { isTestnetModeEnabled } = useEnabledChains() @@ -78,21 +78,6 @@ export default function AuthenticatedHeader({ const accountDrawer = useAccountDrawer() - const openReceiveCryptoModal = useOpenReceiveCryptoModal({ - modalState: ReceiveModalState.DEFAULT, - }) - - const { navigateToSendFlow } = useUniswapContext() - - const isSolanaOnlyWallet = Boolean(svmAddress && !evmAddress) - - const onPressSend = useEvent(() => { - if (!isSolanaOnlyWallet) { - navigateToSendFlow({ chainId: UniverseChainId.Mainnet }) - accountDrawer.close() - } - }) - const { data, networkStatus, loading } = usePortfolioTotalValue({ evmAddress, svmAddress, @@ -115,13 +100,18 @@ export default function AuthenticatedHeader({ const isPermitMismatchUxEnabled = useFeatureFlag(FeatureFlags.EnablePermitMismatchUX) const shouldShowDelegationMismatch = isPermitMismatchUxEnabled && isDelegationMismatch const [displayDelegationMismatchModal, setDisplayDelegationMismatchModal] = useState(false) - const theme = useTheme() + const colors = useSporeColors() const amount = unclaimedAmount?.toFixed(0, { groupSeparator: ',' }) ?? '-' const shouldFadePortfolioDecimals = (currency === FiatCurrency.UnitedStatesDollar || currency === FiatCurrency.Euro) && currencyComponents.symbolAtFront + const handleOpenExtensionSidebar = useCallback(() => { + uniswapExtensionRequest?.(ExtensionRequestMethods.OPEN_SIDEBAR, 'Tokens') + accountDrawer.close() + }, [uniswapExtensionRequest, accountDrawer]) + return ( <> @@ -133,11 +123,23 @@ export default function AuthenticatedHeader({ size={48} /> + {shouldShowExtensionButton && ( + } + borderRadius="$rounded32" + hoverStyle={{ + backgroundColor: '$surface2', + }} + onPress={handleOpenExtensionSidebar} + /> + )} } + icon={} borderRadius="$rounded32" hoverStyle={{ backgroundColor: '$surface2', @@ -189,24 +191,19 @@ export default function AuthenticatedHeader({ ) : ( <> - - } - name={t('common.send.button')} - onClick={onPressSend} - disabled={isSolanaOnlyWallet} - /> - - } - name={t('common.receive')} - onClick={openReceiveCryptoModal} - /> + + + + + + - + {isPortfolioPageEnabled ? ( + + ) : ( + + )} )} {isUnclaimed && ( @@ -216,7 +213,7 @@ export default function AuthenticatedHeader({ onPress={toggleClaimModal} style={{ background: 'linear-gradient(to right, #9139b0 0%, #4261d6 100%)' }} > - + {t('account.authHeader.claimReward', { amount })} )} diff --git a/apps/web/src/components/AccountDrawer/DisconnectButton.tsx b/apps/web/src/components/AccountDrawer/DisconnectButton.tsx index 0e23c16c916..4b1ebc00bd3 100644 --- a/apps/web/src/components/AccountDrawer/DisconnectButton.tsx +++ b/apps/web/src/components/AccountDrawer/DisconnectButton.tsx @@ -3,17 +3,17 @@ import { useAccountDrawer } from 'components/AccountDrawer/MiniPortfolio/hooks' import { MenuStateVariant, useSetMenu } from 'components/AccountDrawer/menuState' import { Power } from 'components/Icons/Power' import { useAccountsStore, useActiveConnector, useActiveWallet } from 'features/accounts/store/hooks' -import { ExternalWallet } from 'features/accounts/store/types' +import { type ExternalWallet } from 'features/accounts/store/types' import { useDisconnect } from 'hooks/useDisconnect' import { useSignOutWithPasskey } from 'hooks/useSignOutWithPasskey' -import { useTheme } from 'lib/styled-components' -import { PropsWithChildren, useMemo } from 'react' +import { type PropsWithChildren, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useDispatch } from 'react-redux' -import { Button, Flex, IconButton, Image, Text, Tooltip } from 'ui/src' +import { Button, Flex, IconButton, Image, Text, Tooltip, useSporeColors } from 'ui/src' import { PlusCircle } from 'ui/src/components/icons/PlusCircle' import { SwitchArrows } from 'ui/src/components/icons/SwitchArrows' -import { AppTFunction } from 'ui/src/i18n/types' +import { type AppTFunction } from 'ui/src/i18n/types' +import { zIndexes } from 'ui/src/theme' import { CONNECTION_PROVIDER_IDS } from 'uniswap/src/constants/web3' import { Platform } from 'uniswap/src/features/platforms/types/Platform' import { setIsTestnetModeEnabled } from 'uniswap/src/features/settings/slice' @@ -78,14 +78,14 @@ export function DisconnectButton() { } function PowerIconButton({ onPress, pointer }: { onPress?: () => void; pointer: boolean }) { - const theme = useTheme() + const colors = useSporeColors() return ( } + icon={} borderRadius="$rounded32" hoverStyle={{ backgroundColor: '$surface2', @@ -100,14 +100,18 @@ function DisconnectMenuTooltip({ children }: PropsWithChildren) { return ( {children} - + ) } -function DisconnectMenuButtonRow({ children, onPress }: PropsWithChildren<{ onPress: () => void }>) { +function DisconnectMenuButtonRow({ + children, + onPress, + testId, +}: PropsWithChildren<{ onPress: () => void; testId?: string }>) { return ( @@ -264,12 +269,12 @@ function SwitchWalletButtonRow({ variant, platform }: { variant: SwitchButtonVar function InLineDisconnectButton() { const onDisconnect = useOnDisconnect() const { t } = useTranslation() - const theme = useTheme() + const colors = useSporeColors() return ( - - + + {t('common.button.disconnect')} diff --git a/apps/web/src/components/AccountDrawer/LocalCurrencyMenu.tsx b/apps/web/src/components/AccountDrawer/LocalCurrencyMenu.tsx index df10d6598c6..2822cafbbba 100644 --- a/apps/web/src/components/AccountDrawer/LocalCurrencyMenu.tsx +++ b/apps/web/src/components/AccountDrawer/LocalCurrencyMenu.tsx @@ -2,13 +2,13 @@ import { SlideOutMenu } from 'components/AccountDrawer/SlideOutMenu' import { MenuColumn, MenuItem } from 'components/AccountDrawer/shared' import { getLocalCurrencyIcon } from 'constants/localCurrencies' import { useLocalCurrencyLinkProps } from 'hooks/useLocalCurrencyLinkProps' -import styled from 'lib/styled-components' +import { deprecatedStyled } from 'lib/styled-components' import { useMemo } from 'react' import { Trans } from 'react-i18next' import { FiatCurrency, ORDERED_CURRENCIES } from 'uniswap/src/features/fiatCurrency/constants' import { useAppFiatCurrency } from 'uniswap/src/features/fiatCurrency/hooks' -const StyledLocalCurrencyIcon = styled.div` +const StyledLocalCurrencyIcon = deprecatedStyled.div` width: 20px; height: 20px; border-radius: 100%; diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/ActivityTab.anvil.e2e.test.ts b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/ActivityTab.anvil.e2e.test.ts deleted file mode 100644 index 432c2f5f253..00000000000 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/ActivityTab.anvil.e2e.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { createExpectSingleTransaction } from 'playwright/anvil/transactions' -import { expect, getTest } from 'playwright/fixtures' -import { stubTradingApiEndpoint } from 'playwright/fixtures/tradingApi' -import { TEST_WALLET_ADDRESS } from 'playwright/fixtures/wallets' -import { Mocks } from 'playwright/mocks/mocks' -import { USDC_MAINNET } from 'uniswap/src/constants/tokens' -import { uniswapUrls } from 'uniswap/src/constants/urls' -import { ElementName } from 'uniswap/src/features/telemetry/constants' -import { TestID } from 'uniswap/src/test/fixtures/testIDs' - -const test = getTest({ withAnvil: true }) - -test.describe('ActivityTab activity history', () => { - test('should deduplicate activity history by nonce', async ({ page, graphql, anvil }) => { - const expectSingleTransaction = createExpectSingleTransaction({ - anvil, - address: TEST_WALLET_ADDRESS, - options: { blocks: 2 }, - }) - - await stubTradingApiEndpoint({ page, endpoint: uniswapUrls.tradingApiPaths.swap }) - await stubTradingApiEndpoint({ page, endpoint: uniswapUrls.tradingApiPaths.quote }) - await graphql.intercept('ActivityWeb', Mocks.Account.activity_history) - await page.goto(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`) - - // Perform swap and verify transaction was submitted - await expectSingleTransaction(async () => { - await page.getByTestId(TestID.AmountInputIn).click() - await page.getByTestId(TestID.AmountInputIn).fill('1') - await expect(page.getByTestId(TestID.AmountInputIn)).toHaveValue('1') - await page.getByTestId(TestID.ReviewSwap).click() - await page.getByTestId(TestID.Swap).click() - await page.getByTestId(TestID.ActivityPopupCloseIcon).click() - }) - - // Open account drawer and navigate to activity tab - await page.getByTestId(TestID.Web3StatusConnected).click() - await page.getByTestId(ElementName.MiniPortfolioActivityTab).click() - - // Wait for activity content to be visible - await expect(page.getByTestId(TestID.ActivityContent)).toBeVisible() - - // Assert that the local pending transaction is replaced by remote transaction with the same nonce - // The "Swapping" text should not exist as it indicates a pending local transaction - await expect(page.getByText('Swapping')).not.toBeVisible() - - // Verify that we have activity content displayed (from the mocked GraphQL response) - await expect(page.getByTestId(TestID.ActivityContent)).toBeVisible() - }) -}) diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/ActivityTab.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/ActivityTab.tsx index b0bcf916c64..fe6f8f27853 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/ActivityTab.tsx +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/ActivityTab.tsx @@ -1,10 +1,11 @@ import { OpenLimitOrdersButton } from 'components/AccountDrawer/MiniPortfolio/Limits/OpenLimitOrdersButton' import { MenuStateVariant, useSetMenu } from 'components/AccountDrawer/menuState' import { useMemo } from 'react' -import { Flex, ScrollView } from 'ui/src' +import { Flex, Loader, ScrollView } from 'ui/src' import { ActivityItem } from 'uniswap/src/components/activity/generateActivityItemRenderer' import { useActivityData } from 'uniswap/src/features/activity/hooks/useActivityData' import { TestID } from 'uniswap/src/test/fixtures/testIDs' +import { useInfiniteScroll } from 'utilities/src/react/useInfiniteScroll' export default function ActivityTab({ evmOwner, @@ -15,17 +16,19 @@ export default function ActivityTab({ }) { const setMenu = useSetMenu() - const { maybeEmptyComponent, renderActivityItem, sectionData } = useActivityData({ - evmOwner, - svmOwner, - ownerAddresses: [evmOwner, svmOwner].filter(Boolean) as string[], - swapCallbacks: { - useLatestSwapTransaction: () => undefined, - useSwapFormTransactionState: () => undefined, - onRetryGenerator: () => () => {}, - }, - fiatOnRampParams: undefined, - skip: false, + const { maybeEmptyComponent, renderActivityItem, sectionData, fetchNextPage, hasNextPage, isFetchingNextPage } = + useActivityData({ + evmOwner, + svmOwner, + ownerAddresses: [evmOwner, svmOwner].filter(Boolean) as string[], + fiatOnRampParams: undefined, + skip: false, + }) + + const { sentinelRef } = useInfiniteScroll({ + onLoadMore: fetchNextPage, + hasNextPage, + isFetching: isFetchingNextPage, }) const ActivityItems = useMemo(() => { @@ -52,6 +55,14 @@ export default function ActivityTab({ )} {ActivityItems} + {/* Show skeleton loading indicator while fetching next page */} + {isFetchingNextPage && ( + + + + )} + {/* Intersection observer sentinel for infinite scroll */} + ) diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/CancelOrdersDialog.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/CancelOrdersDialog.tsx index 0defc585b6d..9a5e7402481 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/CancelOrdersDialog.tsx +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/CancelOrdersDialog.tsx @@ -6,12 +6,13 @@ import { ColumnCenter } from 'components/deprecated/Column' import Row from 'components/deprecated/Row' import { LoaderV3 } from 'components/Icons/LoadingSpinner' import { DetailLineItem } from 'components/swap/DetailLineItem' -import styled, { useTheme } from 'lib/styled-components' +import { deprecatedStyled } from 'lib/styled-components' +import { useMemo } from 'react' import { Slash } from 'react-feather' import { Trans, useTranslation } from 'react-i18next' import { ThemedText } from 'theme/components' import { ExternalLink } from 'theme/components/Links' -import { Flex, Text } from 'ui/src' +import { Flex, Text, useSporeColors } from 'ui/src' import { Dialog } from 'uniswap/src/components/dialog/Dialog' import { GetHelpHeader } from 'uniswap/src/components/dialog/GetHelpHeader' import { Modal } from 'uniswap/src/components/modals/Modal' @@ -24,11 +25,11 @@ import { UniswapXOrderDetails } from 'uniswap/src/features/transactions/types/tr import { ExplorerDataType, getExplorerLink } from 'uniswap/src/utils/linking' import { NumberType } from 'utilities/src/format/types' -const ModalHeader = styled(GetHelpHeader)` +const ModalHeader = deprecatedStyled(GetHelpHeader)` padding: 4px 0px; ` -const Container = styled(ColumnCenter)` +const Container = deprecatedStyled(ColumnCenter)` background-color: ${({ theme }) => theme.surface1}; border-radius: 16px; padding: 16px 24px 24px 24px; @@ -56,7 +57,7 @@ function useCancelOrdersDialogContent( state: CancellationState, orders: UniswapXOrderDetails[], ): { title?: JSX.Element; icon: JSX.Element } { - const theme = useTheme() + const colors = useSporeColors() switch (state) { case CancellationState.REVIEWING_CANCELLATION: return { @@ -66,12 +67,12 @@ function useCancelOrdersDialogContent( ) : ( ), - icon: , + icon: , } case CancellationState.PENDING_SIGNATURE: return { title: , - icon: , + icon: , } case CancellationState.PENDING_CONFIRMATION: return { @@ -98,6 +99,22 @@ export function CancelOrdersDialog(props: CancelOrdersDialogProps) { const { title, icon } = useCancelOrdersDialogContent(cancelState, orders) const cancellationGasFeeInfo = useCancelOrdersGasEstimate(orders) + + const primaryButton = useMemo( + () => ({ + text: t('common.neverMind'), + onPress: onCancel, + variant: 'default' as const, + emphasis: 'secondary' as const, + }), + [t, onCancel], + ) + + const secondaryButton = useMemo( + () => ({ text: t('common.proceed'), onPress: onConfirm, variant: 'critical' as const }), + [t, onConfirm], + ) + if ( [CancellationState.PENDING_SIGNATURE, CancellationState.PENDING_CONFIRMATION, CancellationState.CANCELLED].includes( cancelState, @@ -155,18 +172,10 @@ export function CancelOrdersDialog(props: CancelOrdersDialogProps) { } modalName={ModalName.CancelOrders} - primaryButtonText={t('common.neverMind')} - primaryButtonOnClick={onCancel} - primaryButtonVariant="default" - primaryButtonEmphasis="secondary" - secondaryButtonText={t('common.proceed')} - secondaryButtonOnClick={onConfirm} - secondaryButtonVariant="critical" - buttonContainerProps={{ - flexDirection: 'row', - }} + primaryButton={primaryButton} + secondaryButton={secondaryButton} displayHelpCTA - hasIconBackground + iconBackgroundColor="$surface3" > {/* eslint-disable-next-line @typescript-eslint/no-unnecessary-condition */} @@ -185,15 +194,7 @@ function GasEstimateDisplay({ gasEstimateValue, chainId }: { gasEstimateValue?: const gasFeeFormatted = convertFiatAmountFormatted(gasFeeUSD?.toExact(), NumberType.PortfolioBalance) return ( - + , diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/Logos.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/Logos.tsx index 9c5a6081336..1fd9c4c0738 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/Logos.tsx +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/Logos.tsx @@ -1,8 +1,9 @@ import { LoaderV3 } from 'components/Icons/LoadingSpinner' -import styled, { css, useTheme } from 'lib/styled-components' +import { css, deprecatedStyled } from 'lib/styled-components' import { FadePresence, FadePresenceAnimationType } from 'theme/components/FadePresence' +import { useSporeColors } from 'ui/src' -export const LogoContainer = styled.div` +export const LogoContainer = deprecatedStyled.div` height: 64px; width: 64px; position: relative; @@ -12,7 +13,7 @@ export const LogoContainer = styled.div` overflow: visible; ` -const LoadingIndicator = styled(LoaderV3)` +const LoadingIndicator = deprecatedStyled(LoaderV3)` stroke: ${({ theme }) => theme.neutral3}; fill: ${({ theme }) => theme.neutral3}; width: calc(100% + 8px); @@ -31,7 +32,7 @@ export function LoadingIndicatorOverlay() { } export function ConfirmedIcon({ className }: { className?: string }) { - const theme = useTheme() + const colors = useSporeColors() return ( @@ -53,7 +54,7 @@ export function ConfirmedIcon({ className }: { className?: string }) { } export function SubmittedIcon({ className }: { className?: string }) { - const theme = useTheme() + const colors = useSporeColors() return ( @@ -79,10 +80,10 @@ const IconCss = css` width: 64px; ` -export const AnimatedEntranceConfirmationIcon = styled(ConfirmedIcon)` +export const AnimatedEntranceConfirmationIcon = deprecatedStyled(ConfirmedIcon)` ${IconCss} ` -export const AnimatedEntranceSubmittedIcon = styled(SubmittedIcon)` +export const AnimatedEntranceSubmittedIcon = deprecatedStyled(SubmittedIcon)` ${IconCss} ` diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/OffchainActivityModal.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/OffchainActivityModal.tsx index 01186b48495..28b1df8a0b8 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/OffchainActivityModal.tsx +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/OffchainActivityModal.tsx @@ -22,14 +22,14 @@ import { useUSDPrice } from 'hooks/useUSDPrice' import { TFunction } from 'i18next' import { atom } from 'jotai' import { useAtomValue, useUpdateAtom } from 'jotai/utils' -import styled, { useTheme } from 'lib/styled-components' +import { deprecatedStyled } from 'lib/styled-components' import { useCallback, useMemo, useState } from 'react' import { ArrowDown } from 'react-feather' import { useTranslation } from 'react-i18next' import { useUniswapXOrderByOrderHash } from 'state/transactions/hooks' import { ThemedText } from 'theme/components' import { Divider } from 'theme/components/Dividers' -import { Button, Flex, TouchableArea } from 'ui/src' +import { Button, Flex, TouchableArea, useSporeColors } from 'ui/src' import { X } from 'ui/src/components/icons/X' import { Modal } from 'uniswap/src/components/modals/Modal' import { InterfaceEventName, ModalName } from 'uniswap/src/features/telemetry/constants' @@ -63,17 +63,17 @@ export function useOpenOffchainActivityModal() { ) } -const Wrapper = styled(AutoColumn).attrs({ gap: 'md', grow: true })` +const Wrapper = deprecatedStyled(AutoColumn).attrs({ gap: 'md', grow: true })` padding: 12px 20px 20px 20px; width: 100%; background-color: ${({ theme }) => theme.surface1}; ` -const OffchainModalDivider = styled(Divider)` +const OffchainModalDivider = deprecatedStyled(Divider)` margin: 28px 0; ` -const InsufficientFundsCopyContainer = styled(Row)` +const InsufficientFundsCopyContainer = deprecatedStyled(Row)` margin-top: 16px; padding: 12px; border: 1.3px solid ${({ theme }) => theme.surface3}; @@ -83,7 +83,7 @@ const InsufficientFundsCopyContainer = styled(Row)` align-items: flex-start; ` -const AlertIconContainer = styled.div` +const AlertIconContainer = deprecatedStyled.div` display: flex; flex-shrink: 0; background-color: ${({ theme }) => theme.deprecated_accentWarning}; @@ -173,7 +173,7 @@ export function OrderContent({ order, onCancel }: { order: UniswapXOrderDetails; const amountsDefined = !!amounts?.inputAmount.currency && !!amounts.outputAmount.currency const fiatValueInput = useUSDPrice(amounts?.inputAmount) const fiatValueOutput = useUSDPrice(amounts?.outputAmount) - const theme = useTheme() + const colors = useSporeColors() const explorerLink = order.hash ? getExplorerLink({ chainId: order.chainId, data: order.hash, type: ExplorerDataType.TRANSACTION }) @@ -241,7 +241,7 @@ export function OrderContent({ order, onCancel }: { order: UniswapXOrderDetails; isLoading={false} headerTextProps={{ fontSize: '24px', lineHeight: '32px' }} /> - + should render limit order text 1`] = ` .c0 { color: #131313; - -webkit-letter-spacing: -0.01em; - -moz-letter-spacing: -0.01em; - -ms-letter-spacing: -0.01em; letter-spacing: -0.01em; } .c1 { cursor: auto; - color: rgba(19,19,19,0.63); + color: rgba(19, 19, 19, 0.63); } .c2 { @@ -19,14 +16,10 @@ exports[`CancelOrdersDialog > should render limit order text 1`] = ` overflow-wrap: break-word; } - + -

{ } } -const TooltipContainer = styled.div<{ size: TooltipSize; padding?: number }>` +const TooltipContainer = deprecatedStyled.div<{ size: TooltipSize; padding?: number }>` max-width: ${({ size }) => size}; width: ${({ size }) => (size === TooltipSize.Max ? 'auto' : `calc(100vw - 16px)`)}; cursor: default; diff --git a/apps/web/src/components/TopLevelBanners/UniswapWrapped2025Banner.tsx b/apps/web/src/components/TopLevelBanners/UniswapWrapped2025Banner.tsx new file mode 100644 index 00000000000..57cf8200a99 --- /dev/null +++ b/apps/web/src/components/TopLevelBanners/UniswapWrapped2025Banner.tsx @@ -0,0 +1,32 @@ +import { FeatureFlags, useFeatureFlag } from '@universe/gating' +import { useLocation, useNavigate } from 'react-router' +import { useAppDispatch, useAppSelector } from 'state/hooks' +import { InterfaceState } from 'state/webReducer' +import { WRAPPED_PATH } from 'uniswap/src/components/banners/shared/utils' +import { UniswapWrapped2025Banner } from 'uniswap/src/components/banners/UniswapWrapped2025Banner/UniswapWrapped2025Banner' +import { selectHasDismissedUniswapWrapped2025Banner } from 'uniswap/src/features/behaviorHistory/selectors' +import { setHasDismissedUniswapWrapped2025Banner } from 'uniswap/src/features/behaviorHistory/slice' + +export function useRenderUniswapWrapped2025Banner(): JSX.Element | null { + const isFeatureFlagEnabled = useFeatureFlag(FeatureFlags.UniswapWrapped2025) + const hasDismissed = useAppSelector((state: InterfaceState) => selectHasDismissedUniswapWrapped2025Banner(state)) + const { pathname } = useLocation() + const isWrappedPage = pathname.startsWith(WRAPPED_PATH) + const dispatch = useAppDispatch() + const navigate = useNavigate() + + const handleDismiss = (): void => { + dispatch(setHasDismissedUniswapWrapped2025Banner(true)) + } + + const handlePress = (): void => { + dispatch(setHasDismissedUniswapWrapped2025Banner(true)) + navigate(WRAPPED_PATH) + } + + if (isFeatureFlagEnabled && !hasDismissed && !isWrappedPage) { + return + } + + return null +} diff --git a/apps/web/src/components/TopLevelModals/index.tsx b/apps/web/src/components/TopLevelModals/index.tsx index 818834c25b9..c708b7c44ac 100644 --- a/apps/web/src/components/TopLevelModals/index.tsx +++ b/apps/web/src/components/TopLevelModals/index.tsx @@ -1,17 +1,24 @@ +import { POPUP_MEDIUM_DISMISS_MS } from 'components/Popups/constants' +import { popupRegistry } from 'components/Popups/registry' +import { PopupType } from 'components/Popups/types' import { ModalRenderer } from 'components/TopLevelModals/modalRegistry' import useAccountRiskCheck from 'hooks/useAccountRiskCheck' import { PageType, useIsPage } from 'hooks/useIsPage' import { PasskeysHelpModalTypeAtom } from 'hooks/usePasskeyAuthWithHelpModal' import { useAtomValue } from 'jotai/utils' +import { useTranslation } from 'react-i18next' import { BridgedAssetModalAtom } from 'uniswap/src/components/BridgedAsset/BridgedAssetModal' import { WormholeModalAtom } from 'uniswap/src/components/BridgedAsset/WormholeModal' +import { ReportTokenIssueModalPropsAtom } from 'uniswap/src/components/reporting/ReportTokenIssueModal' import { useUnitagsAddressQuery } from 'uniswap/src/data/apiClients/unitagsApi/useUnitagsAddressQuery' import { ModalName } from 'uniswap/src/features/telemetry/constants' import { useWallet } from 'uniswap/src/features/wallet/hooks/useWallet' import { shortenAddress } from 'utilities/src/addresses' import { isBetaEnv, isDevEnv } from 'utilities/src/environment/env' +import { useEvent } from 'utilities/src/react/hooks' export default function TopLevelModals() { + const { t } = useTranslation() const isLandingPage = useIsPage(PageType.LANDING) const wallet = useWallet() const evmAddress = wallet.evmAccount?.address @@ -29,6 +36,15 @@ export default function TopLevelModals() { const bridgedAssetModalProps = useAtomValue(BridgedAssetModalAtom) const wormholeModalProps = useAtomValue(WormholeModalAtom) + const reportTokenIssueProps = useAtomValue(ReportTokenIssueModalPropsAtom) + const onReportSuccess = useEvent(() => { + popupRegistry.addPopup( + { type: PopupType.Success, message: t('common.reported') }, + 'report-token-success', + POPUP_MEDIUM_DISMISS_MS, + ) + }) + const shouldShowDevFlags = isDevEnv() || isBetaEnv() // On landing page we need to be very careful about what modals we show @@ -83,6 +99,10 @@ export default function TopLevelModals() { + ) } diff --git a/apps/web/src/components/TopLevelModals/modalRegistry.test.tsx b/apps/web/src/components/TopLevelModals/modalRegistry.test.tsx index ec5fc22d676..62c1c0b78d6 100644 --- a/apps/web/src/components/TopLevelModals/modalRegistry.test.tsx +++ b/apps/web/src/components/TopLevelModals/modalRegistry.test.tsx @@ -20,9 +20,9 @@ vi.mock('components/AccountDrawer/UniwalletModal', () => ({ default: () =>
Uniwallet Modal
, })) -vi.mock('components/Banner/shared/Banners', () => ({ +vi.mock('components/Banner/shared/OutageBanners', () => ({ __esModule: true, - Banners: () =>
Banners
, + OutageBanners: () =>
Outage Banners
, })) vi.mock('components/AccountDrawer/MiniPortfolio/Activity/OffchainActivityModal', () => ({ @@ -80,7 +80,7 @@ describe('ModalRegistry', () => { await act(async () => { render() }) - expect(screen.getByTestId('mock-banners')).toBeInTheDocument() + expect(screen.getByTestId('mock-outage-banners')).toBeInTheDocument() }) it('renders modals with custom props', async () => { diff --git a/apps/web/src/components/TopLevelModals/modalRegistry.tsx b/apps/web/src/components/TopLevelModals/modalRegistry.tsx index a861f540381..f049490c56d 100644 --- a/apps/web/src/components/TopLevelModals/modalRegistry.tsx +++ b/apps/web/src/components/TopLevelModals/modalRegistry.tsx @@ -1,8 +1,10 @@ +import ErrorBoundary from 'components/ErrorBoundary' import { ModalRegistry, ModalWrapperProps } from 'components/TopLevelModals/types' import { useModalState } from 'hooks/useModalState' import { memo, Suspense } from 'react' import { useAppSelector } from 'state/hooks' import { ModalName, ModalNameType } from 'uniswap/src/features/telemetry/constants' +import { logger } from 'utilities/src/logger/logger' import { createLazy } from 'utils/lazyWithRetry' const AddressClaimModal = createLazy(() => import('components/claim/AddressClaimModal')) @@ -11,8 +13,8 @@ const PendingWalletConnectionModal = createLazy( () => import('components/WalletModal/PendingWalletConnectionModal/PendingWalletConnectionModal'), ) const UniwalletModal = createLazy(() => import('components/AccountDrawer/UniwalletModal')) -const Banners = createLazy(() => - import('components/Banner/shared/Banners').then((module) => ({ default: module.Banners })), +const OutageBanners = createLazy(() => + import('components/Banner/shared/OutageBanners').then((module) => ({ default: module.OutageBanners })), ) const OffchainActivityModal = createLazy(() => import('components/AccountDrawer/MiniPortfolio/Activity/OffchainActivityModal').then((module) => ({ @@ -35,7 +37,6 @@ const PrivacyChoicesModal = createLazy(() => import('components/PrivacyChoices').then((module) => ({ default: module.PrivacyChoicesModal })), ) const FeatureFlagModal = createLazy(() => import('components/FeatureFlagModal/FeatureFlagModal')) -const SolanaPromoModal = createLazy(() => import('components/Banner/SolanaPromo/SolanaPromoModal')) const DevFlagsBox = createLazy(() => import('dev/DevFlagsBox')) const TokenNotFoundModal = createLazy(() => import('components/NotFoundModal/TokenNotFoundModal')) const PoolNotFoundModal = createLazy(() => import('components/NotFoundModal/PoolNotFoundModal')) @@ -83,13 +84,34 @@ const WormholeModal = createLazy(() => })), ) -const ModalLoadingFallback = memo(() => null) -ModalLoadingFallback.displayName = 'ModalLoadingFallback' +const ReportTokenModal = createLazy(() => + import('uniswap/src/components/reporting/ReportTokenIssueModal').then((module) => ({ + default: module.ReportTokenIssueModal, + })), +) +function ModalLoadingFallback(): null { + return null +} + +function ModalErrorFallback({ error }: { error: Error }): null { + logger.error(error, { + tags: { + file: 'modalRegistry', + function: 'ModalErrorFallback', + }, + extra: { + message: 'Modal failed to load - error caught by ErrorBoundary. Modal will not be displayed.', + }, + }) + return null +} const ModalWrapper = memo(({ Component, componentProps }: ModalWrapperProps) => ( - }> - - + + }> + + + )) ModalWrapper.displayName = 'ModalWrapper' @@ -108,7 +130,7 @@ export const modalRegistry: ModalRegistry = { shouldMount: () => true, }, [ModalName.Banners]: { - component: Banners, + component: OutageBanners, shouldMount: () => true, }, [ModalName.OffchainActivity]: { @@ -143,10 +165,6 @@ export const modalRegistry: ModalRegistry = { component: FeatureFlagModal, shouldMount: (state) => state.application.openModal?.name === ModalName.FeatureFlags, }, - [ModalName.SolanaPromo]: { - component: SolanaPromoModal, - shouldMount: (state) => state.application.openModal?.name === ModalName.SolanaPromo, - }, [ModalName.AddLiquidity]: { component: IncreaseLiquidityModal, shouldMount: (state) => state.application.openModal?.name === ModalName.AddLiquidity, @@ -199,6 +217,10 @@ export const modalRegistry: ModalRegistry = { component: WormholeModal, shouldMount: (state) => state.application.openModal?.name === ModalName.Wormhole, }, + [ModalName.ReportTokenIssue]: { + component: ReportTokenModal, + shouldMount: (state) => state.application.openModal?.name === ModalName.ReportTokenIssue, + }, } as const export const ModalRenderer = ({ diff --git a/apps/web/src/components/TopLevelModals/types.ts b/apps/web/src/components/TopLevelModals/types.ts index 7b228811d22..07cf60276b2 100644 --- a/apps/web/src/components/TopLevelModals/types.ts +++ b/apps/web/src/components/TopLevelModals/types.ts @@ -1,7 +1,7 @@ import { ComponentProps, ComponentType } from 'react' import { ModalNameType } from 'uniswap/src/features/telemetry/constants' -type ModalComponent = React.LazyExoticComponent> +type ModalComponent = ComponentType interface ModalConfig { component: ModalComponent diff --git a/apps/web/src/components/Toucan/Auction/AuctionStats/AuctionStats.tsx b/apps/web/src/components/Toucan/Auction/AuctionStats/AuctionStats.tsx new file mode 100644 index 00000000000..8534f8eb3d2 --- /dev/null +++ b/apps/web/src/components/Toucan/Auction/AuctionStats/AuctionStats.tsx @@ -0,0 +1,293 @@ +import { AuctionStatsData, FAKE_AUCTION_STATS } from 'components/Toucan/Auction/store/mockData' +import { TFunction } from 'i18next' +import { deprecatedStyled } from 'lib/styled-components' +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { CopyHelper } from 'theme/components/CopyHelper' +import { ExternalLink } from 'theme/components/Links' +import { ClickableTamaguiStyle } from 'theme/components/styles' +import { Flex, styled, Text } from 'ui/src' +import { Globe } from 'ui/src/components/icons/Globe' +import { XTwitter } from 'ui/src/components/icons/XTwitter' +import { shortenAddress } from 'utilities/src/addresses' + +interface StatItem { + label: string + value: string | undefined +} + +/** + * Builds the list of stat items to display, filtering out undefined values + * @param stats - The auction stats data + * @param t - Translation function + * @returns Array of stat items with labels and values + */ +function buildStatItems(stats: AuctionStatsData, t: TFunction): StatItem[] { + return [ + { + label: t('toucan.auction.impliedTokenPrice'), + value: + stats.impliedTokenPriceMin && stats.impliedTokenPriceMax + ? `${stats.impliedTokenPriceMin} – ${stats.impliedTokenPriceMax}` + : undefined, + }, + { + label: t('toucan.auction.totalBids'), + value: stats.totalBids.toLocaleString(), + }, + { + label: t('toucan.auction.placeholderStat'), + value: 'xx,xxx', + }, + { + label: t('toucan.auction.placeholderStat'), + value: 'xx,xxx', + }, + { + label: t('toucan.auction.circulatingSupply'), + value: stats.circulatingSupply, + }, + { + label: t('toucan.auction.totalSupply'), + value: stats.totalSupply, + }, + ].filter((item): item is StatItem => Boolean(item.value)) +} + +const STATS_PER_ROW = 3 + +const StatsGrid = styled(Flex, { + width: '100%', + '$platform-web': { + display: 'grid', + gridTemplateColumns: '1fr 1fr 1fr', + }, +}) + +const StatCell = styled(Flex, { + paddingVertical: '$spacing12', + gap: '$spacing2', + borderRightWidth: 1, + borderColor: '$surface3', + $md: { + paddingVertical: '$spacing8', + }, + variants: { + // Position-based variants for grid layout + isLastInRow: { + true: { + borderRightWidth: 0, + }, + }, + isFirstRow: { + true: { + borderBottomWidth: 1, + borderColor: '$surface3', + }, + }, + hasLeftPadding: { + true: { + paddingLeft: '$spacing12', + }, + }, + } as const, +}) + +const InfoRow = styled(Flex, { + width: '100%', + '$platform-web': { + display: 'grid', + gridTemplateColumns: '1fr 1fr 1fr', + }, +}) + +const InfoCell = styled(Flex, { + gap: '$spacing2', + paddingVertical: '$spacing2', + $md: { + paddingVertical: '$spacing2', + }, + variants: { + withBorder: { + true: { + borderLeftWidth: 1, + borderColor: '$surface3', + paddingHorizontal: '$spacing16', + }, + }, + } as const, +}) + +const SocialBadge = styled(Text, { + variant: 'buttonLabel3', + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + gap: '$spacing8', + paddingHorizontal: '$spacing12', + height: 32, + borderRadius: '$rounded20', + backgroundColor: '$surface3', + ...ClickableTamaguiStyle, + color: '$neutral1', +}) + +const CompanyIcon = styled(Flex, { + width: 16, + height: 16, + borderRadius: '$roundedFull', + backgroundColor: '$accent1', + alignItems: 'center', + justifyContent: 'center', +}) + +// Override ExternalLink's pink stroke to prevent it from affecting child SVG icons +const StyledExternalLink = deprecatedStyled(ExternalLink)` + stroke: none; +` + +export const AuctionStats = () => { + const { t } = useTranslation() + const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false) + const stats = FAKE_AUCTION_STATS + + // TODO | Toucan: use actual data + const statItems = buildStatItems(stats, t) + + const totalStats = statItems.length + + return ( + + {/* Header */} + {t('toucan.auction.stats')} + {/* Stats Table */} + + {statItems.map((item, index) => { + // Calculate grid position for border/padding logic + const col = index % STATS_PER_ROW + const isInFirstRow = index < STATS_PER_ROW + + return ( + STATS_PER_ROW} + hasLeftPadding={col !== 0} + > + + {item.label} + + + {item.value} + + + ) + })} + + + {/* Info Section */} + + {t('toucan.auction.info')} + + {/* Launched by / Launched on / Contract address row */} + + + + {t('toucan.auction.launchedBy')} + + + + + F + + + + {stats.launchedBy.name} + + + + + + + {t('toucan.auction.launchedOn')} + + + {stats.launchedOn} + + + + + + {t('toucan.auction.contractAddress')} + + + + + {shortenAddress({ address: stats.contractAddress, chars: 4 })} + + + + + + + {/* Description */} + + + {t('toucan.auction.description')} + + + {stats.description} + + setIsDescriptionExpanded(!isDescriptionExpanded)} + cursor="pointer" + hoverStyle={{ color: '$neutral1' }} + > + {isDescriptionExpanded ? t('toucan.auction.showLess') : t('toucan.auction.showMore')} + + + + {/* Social badges */} + {/* TODO | Toucan - add analytics tracking for social link clicks */} + + {stats.website && ( + + + + {t('toucan.auction.website')} + + + )} + {stats.twitter && ( + + + + {t('toucan.auction.twitter')} + + + )} + + + + ) +} diff --git a/apps/web/src/components/Toucan/Auction/BidActivities/BidActivities.tsx b/apps/web/src/components/Toucan/Auction/BidActivities/BidActivities.tsx new file mode 100644 index 00000000000..366639b8224 --- /dev/null +++ b/apps/web/src/components/Toucan/Auction/BidActivities/BidActivities.tsx @@ -0,0 +1,42 @@ +import { BidActivity } from 'components/Toucan/Auction/BidActivities/BidActivity' +import { FAKE_BID_ACTIVITIES } from 'components/Toucan/Auction/store/mockData' +import { useAuctionStore } from 'components/Toucan/Auction/store/useAuctionStore' +import { useTranslation } from 'react-i18next' +import { Flex, Text } from 'ui/src' +import { useColorHexFromThemeKey } from 'ui/src/hooks/useColorHexFromThemeKey' + +export const BidActivities = () => { + const { t } = useTranslation() + const displayMode = useAuctionStore((state) => state.displayMode) + const surface1 = useColorHexFromThemeKey('surface1') + + return ( + + {t('toucan.auction.bidActivity')} + + + {FAKE_BID_ACTIVITIES.map((activity, index) => ( + + ))} + + + {/* Gradient overlay - fades from transparent at top to surface1 at bottom */} + + + + ) +} diff --git a/apps/web/src/components/Toucan/Auction/BidActivities/BidActivity.tsx b/apps/web/src/components/Toucan/Auction/BidActivities/BidActivity.tsx new file mode 100644 index 00000000000..1b7989772f2 --- /dev/null +++ b/apps/web/src/components/Toucan/Auction/BidActivities/BidActivity.tsx @@ -0,0 +1,59 @@ +import { useAbbreviatedTimeString } from 'components/Table/utils' +import { BidActivity as BidActivityType } from 'components/Toucan/Auction/store/mockData' +import { DisplayMode } from 'components/Toucan/Auction/store/types' +import { useRef } from 'react' +import { Flex, Text, Unicon } from 'ui/src' +import { ONE_SECOND_MS } from 'utilities/src/time/time' + +interface BidActivityProps { + activity: BidActivityType + displayMode?: DisplayMode // TODO | Toucan: make this required once updated to use this +} + +export const BidActivity = ({ activity }: BidActivityProps) => { + // Convert unix timestamp to relative time string (e.g., "1s ago", "2m ago") + const calculatedTimeAgo = useAbbreviatedTimeString(activity.timestamp * ONE_SECOND_MS) + const timeAgoRef = useRef(calculatedTimeAgo) + const timeAgo = timeAgoRef.current + + // TODO | Toucan: Format price based on displayMode + // - DisplayMode.VALUATION: show as "@ 2.5M" or "@ 2.5B" (market cap) + // - DisplayMode.TOKEN_PRICE: show as "@ $2.50" (fiat price per token with user's selected currency) + const formattedPrice = activity.price // Currently showing mock data, needs proper formatting + + return ( + + {/* Left side: Icon + Bid info */} + + + + + {/* TODO | Toucan: Update to use actual BidToken name instead of hardcoded USDC */} + {activity.bidVolume} USDC + + + @ + + + {formattedPrice} + + + + + {/* Right side: Timestamp */} + + + {timeAgo} + + + + ) +} diff --git a/apps/web/src/components/Toucan/Auction/BidDistributionChart/BidDistributionChart.tsx b/apps/web/src/components/Toucan/Auction/BidDistributionChart/BidDistributionChart.tsx index cacd0a245c8..572af7fc5b0 100644 --- a/apps/web/src/components/Toucan/Auction/BidDistributionChart/BidDistributionChart.tsx +++ b/apps/web/src/components/Toucan/Auction/BidDistributionChart/BidDistributionChart.tsx @@ -1,10 +1,103 @@ +import { BidDistributionChartFooter } from 'components/Toucan/Auction/BidDistributionChart/BidDistributionChartFooter' import { BidDistributionChartHeader } from 'components/Toucan/Auction/BidDistributionChart/BidDistributionChartHeader' -import { Flex } from 'ui/src' +import { BidDistributionChartRenderer } from 'components/Toucan/Auction/BidDistributionChart/BidDistributionChartRenderer' +import { generateChartData } from 'components/Toucan/Auction/BidDistributionChart/utils/utils' +import { useBidTokenInfo } from 'components/Toucan/Auction/hooks/useBidTokenInfo' +import { FAKE_AUCTION_DATA } from 'components/Toucan/Auction/store/mockData' +import { useMockDataStore } from 'components/Toucan/Auction/store/mocks/useMockDataStore' +import { useAuctionStore } from 'components/Toucan/Auction/store/useAuctionStore' +import { useMemo } from 'react' +import { Flex, Text } from 'ui/src' +import { EVMUniverseChainId } from 'uniswap/src/features/chains/types' +import { logger } from 'utilities/src/logger/logger' export const BidDistributionChart = () => { + const { displayMode, tokenColor } = useAuctionStore((state) => ({ + displayMode: state.displayMode, + tokenColor: state.tokenColor, + })) + // TODO | Toucan: Remove mock data store usage once live + const { selectedDataset, selectedDatasetIndex, bidTokenAddress, tickSize, clearingPrice, totalSupply } = + useMockDataStore() + + const { + bidTokenInfo, + loading: bidTokenLoading, + error: bidTokenError, + } = useBidTokenInfo(bidTokenAddress, FAKE_AUCTION_DATA.chainId) + + // Calculate chart data once - shared between header and renderer + // Use a simple formatter here since the renderer will apply proper formatting + const chartData = useMemo( + () => + bidTokenInfo + ? generateChartData({ + bidData: selectedDataset, + bidTokenInfo, + displayMode, + totalSupply, + auctionTokenDecimals: FAKE_AUCTION_DATA.tokenDecimals, + clearingPrice, + tickSize, + formatter: (amount: number) => amount.toString(), + }) + : null, + [displayMode, selectedDataset, bidTokenInfo, clearingPrice, tickSize, totalSupply], + ) + + // TODO | Toucan - get error state from design + add translation + // Show error state if bid token info fetch failed + if (bidTokenError) { + logger.error(bidTokenError, { + tags: { file: 'BidDistributionChart', function: 'useBidTokenInfo' }, + extra: { bidTokenAddress, chainId: FAKE_AUCTION_DATA.chainId }, + }) + return ( + + + Failed to load bid token data + + + ) + } + + // TODO | Toucan - get loading state from design + add translation + // Show loading state while fetching bid token info + if (bidTokenLoading || !bidTokenInfo || !chartData) { + return ( + + + Loading bid token data... + + + ) + } + return ( - - + + + + ) } diff --git a/apps/web/src/components/Toucan/Auction/BidDistributionChart/BidDistributionChartFooter.tsx b/apps/web/src/components/Toucan/Auction/BidDistributionChart/BidDistributionChartFooter.tsx new file mode 100644 index 00000000000..27ebeeb0394 --- /dev/null +++ b/apps/web/src/components/Toucan/Auction/BidDistributionChart/BidDistributionChartFooter.tsx @@ -0,0 +1,55 @@ +import { BlockUpdateCountdown } from 'components/Toucan/Auction/BidDistributionChart/BlockUpdateCountdown' +import { BidConcentrationResult } from 'components/Toucan/Auction/BidDistributionChart/utils/bidConcentration' +import { AuctionProgressState } from 'components/Toucan/Auction/store/types' +import { useAuctionStore } from 'components/Toucan/Auction/store/useAuctionStore' +import { useTranslation } from 'react-i18next' +import { Flex, styled, Text } from 'ui/src' +import { EVMUniverseChainId } from 'uniswap/src/features/chains/types' + +const ColorDot = styled(Flex, { + width: 8, + height: 8, + borderRadius: '$roundedFull', +}) + +const HorizontalLine = styled(Flex, { + width: '100%', + height: 1, + backgroundColor: '$surface3', +}) + +interface BidDistributionChartFooterProps { + concentration: BidConcentrationResult | null + chainId: EVMUniverseChainId | undefined +} + +export const BidDistributionChartFooter = ({ concentration, chainId }: BidDistributionChartFooterProps) => { + const { t } = useTranslation() + const tokenColor = useAuctionStore((state) => state.tokenColor) + const auctionState = useAuctionStore((state) => state.progress.state) + + // Hide concentration percentage when auction hasn't started + const shouldShowConcentration = auctionState !== AuctionProgressState.NOT_STARTED && concentration + + return ( + + + + {shouldShowConcentration ? ( + + + + {t('toucan.auction.bidVolume', { percentage: Math.round(concentration.percentage * 100) })} + + + ) : null} + + + + ) +} diff --git a/apps/web/src/components/Toucan/Auction/BidDistributionChart/BidDistributionChartHeader.tsx b/apps/web/src/components/Toucan/Auction/BidDistributionChart/BidDistributionChartHeader.tsx index d6fa6c523f0..f5f20f53187 100644 --- a/apps/web/src/components/Toucan/Auction/BidDistributionChart/BidDistributionChartHeader.tsx +++ b/apps/web/src/components/Toucan/Auction/BidDistributionChart/BidDistributionChartHeader.tsx @@ -1,9 +1,15 @@ -import { DisplayMode } from 'components/Toucan/Auction/store/types' +import { MockDataSelectorModal } from 'components/Toucan/Auction/BidDistributionChart/dev/MockDataSelectorModal' +import { BidConcentrationResult } from 'components/Toucan/Auction/BidDistributionChart/utils/bidConcentration' +import { formatTickForDisplay } from 'components/Toucan/Auction/BidDistributionChart/utils/utils' +import { AuctionProgressState, BidTokenInfo, DisplayMode } from 'components/Toucan/Auction/store/types' import { useAuctionStore, useAuctionStoreActions } from 'components/Toucan/Auction/store/useAuctionStore' import { useTranslation } from 'react-i18next' import { Flex, SegmentedControl, Text, TouchableArea } from 'ui/src' +import { AnglesMaximize } from 'ui/src/components/icons/AnglesMaximize' import { QuestionInCircleFilled } from 'ui/src/components/icons/QuestionInCircleFilled' import { InfoTooltip } from 'uniswap/src/components/tooltip/InfoTooltip' +import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext' +import { NumberType } from 'utilities/src/format/types' const BidDistributionChartHeaderTooltip = () => { const { t } = useTranslation() @@ -29,12 +35,26 @@ const BidDistributionChartHeaderTooltip = () => { ) } -export const BidDistributionChartHeader = () => { +interface BidDistributionChartHeaderProps { + concentration: BidConcentrationResult | null + displayMode: DisplayMode + bidTokenInfo: BidTokenInfo + totalSupply?: string + auctionTokenDecimals?: number +} + +export const BidDistributionChartHeader = ({ + concentration, + displayMode, + bidTokenInfo, + totalSupply, + auctionTokenDecimals = 18, +}: BidDistributionChartHeaderProps) => { const { t } = useTranslation() - const { displayMode } = useAuctionStore((state) => ({ - displayMode: state.displayMode, - })) - const { setDisplayMode } = useAuctionStoreActions() + const { setDisplayMode, resetChartZoom } = useAuctionStoreActions() + const { convertFiatAmountFormatted } = useLocalizationContext() + const isZoomed = useAuctionStore((state) => state.chartZoomState.isZoomed) + const auctionState = useAuctionStore((state) => state.progress.state) const displayModeOptions = [ { @@ -51,14 +71,49 @@ export const BidDistributionChartHeader = () => { setDisplayMode(option) } + // Format concentration range values with user's selected currency + const formatter = (amount: number): string => { + return convertFiatAmountFormatted(amount, NumberType.FiatTokenStats) + } + + const startValue = concentration + ? formatTickForDisplay({ + tickValue: concentration.startTick, + displayMode, + bidTokenInfo, + totalSupply, + auctionTokenDecimals, + formatter, + }) + : null + + const endValue = concentration + ? formatTickForDisplay({ + tickValue: concentration.endTick, + displayMode, + bidTokenInfo, + totalSupply, + auctionTokenDecimals, + formatter, + }) + : null + return ( - - + + {t('toucan.auction.bidConcentration')} + {/* TODO | Toucan: Remove mock data selector once live */} + + {/* Reset zoom button - only visible when chart is zoomed */} + {isZoomed && ( + + + + )} { size="xsmall" /> - - $1.0M - - - - - $2.5M - + {auctionState === AuctionProgressState.NOT_STARTED ? ( + + + -- + + + ) : concentration && startValue && endValue ? ( + + {startValue} + + - + + {endValue} + + ) : ( + + + {t('toucan.auction.noConcentration')} + + + )} ) } diff --git a/apps/web/src/components/Toucan/Auction/BidDistributionChart/BidDistributionChartPlaceholder.tsx b/apps/web/src/components/Toucan/Auction/BidDistributionChart/BidDistributionChartPlaceholder.tsx new file mode 100644 index 00000000000..c22303e4760 --- /dev/null +++ b/apps/web/src/components/Toucan/Auction/BidDistributionChart/BidDistributionChartPlaceholder.tsx @@ -0,0 +1,50 @@ +import { useTranslation } from 'react-i18next' +import { Flex, styled, Text } from 'ui/src' +import { zIndexes } from 'ui/src/theme' + +const PlaceholderBar = styled(Flex, { + width: '100%', + backgroundColor: '$surface2', + borderTopLeftRadius: '$rounded6', + borderTopRightRadius: '$rounded6', + flexShrink: 1, + flexGrow: 1, + flexBasis: 0, + mx: '$spacing2', +}) + +interface BidDistributionChartPlaceholderProps { + height?: number +} + +export function BidDistributionChartPlaceholder({ height = 400 }: BidDistributionChartPlaceholderProps) { + const { t } = useTranslation() + const shortestBarHeight = 20 + const patternIterations = 3 + + const pattern = [1, 1.25, 1.5, 1.75, 1.75, 1.5] + const barHeights = Array.from( + { length: patternIterations * pattern.length }, + (_, i) => shortestBarHeight * pattern[i % pattern.length], + ) + + return ( + + {barHeights.map((barHeight, index) => ( + + ))} + + + {t('toucan.auction.notStarted')} + + + + ) +} diff --git a/apps/web/src/components/Toucan/Auction/BidDistributionChart/BidDistributionChartRenderer.tsx b/apps/web/src/components/Toucan/Auction/BidDistributionChart/BidDistributionChartRenderer.tsx new file mode 100644 index 00000000000..59e3f2deecf --- /dev/null +++ b/apps/web/src/components/Toucan/Auction/BidDistributionChart/BidDistributionChartRenderer.tsx @@ -0,0 +1,770 @@ +/* eslint-disable max-lines */ +import { ToucanChartData } from 'components/Charts/ToucanChart/renderer' +import { ToucanChartSeries } from 'components/Charts/ToucanChart/toucan-chart-series' +import { BidDistributionChartPlaceholder } from 'components/Toucan/Auction/BidDistributionChart/BidDistributionChartPlaceholder' +import { + CHART_PADDING, + CHART_SCALE_MARGINS, + COORDINATE_SCALING, + LABEL_CONFIG, + ZOOM_DEFAULTS, + ZOOM_TOLERANCE, +} from 'components/Toucan/Auction/BidDistributionChart/constants' +import { useChartDimensions } from 'components/Toucan/Auction/BidDistributionChart/hooks/useChartDimensions' +import { useChartLabels } from 'components/Toucan/Auction/BidDistributionChart/hooks/useChartLabels' +import { useChartTooltip } from 'components/Toucan/Auction/BidDistributionChart/hooks/useChartTooltip' +import { BidConcentrationResult } from 'components/Toucan/Auction/BidDistributionChart/utils/bidConcentration' +import { formatClearingPriceLabel } from 'components/Toucan/Auction/BidDistributionChart/utils/clearingPrice/label' +import { + calculateInitialVisibleRange, + generateChartData, +} from 'components/Toucan/Auction/BidDistributionChart/utils/utils' +import { + AuctionProgressState, + BidDistributionData, + BidTokenInfo, + DisplayMode, +} from 'components/Toucan/Auction/store/types' +import { useAuctionStore, useAuctionStoreActions } from 'components/Toucan/Auction/store/useAuctionStore' +import { deprecatedStyled } from 'lib/styled-components' +import { ColorType, createChart, IChartApi, ISeriesApi, LineStyle, UTCTimestamp } from 'lightweight-charts' +import { useCallback, useEffect, useMemo, useRef } from 'react' +import { Flex, useSporeColors } from 'ui/src' +import { UseSporeColorsReturn } from 'ui/src/hooks/useSporeColors' +import { opacify } from 'ui/src/theme' +import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext' +import { NumberType } from 'utilities/src/format/types' +import { logger } from 'utilities/src/logger/logger' +import { formatUnits } from 'viem' + +const ChartContainer = deprecatedStyled.div<{ height: number }>` + width: 100%; + height: ${({ height }) => height}px; + + /* Add padding to lightweight-charts container to prevent top label cutoff */ + .tv-lightweight-charts { + padding-top: 10px; + } + + /* Shift y-axis canvas closer to the chart */ + .tv-lightweight-charts td:first-child canvas { + left: 5px !important; + top: -5px !important; + } + + /* Prevent y-axis labels from being cut off */ + .tv-lightweight-charts td:first-child > div { + overflow: visible !important; + } +` + +interface CrosshairMoveParams { + point?: { x: number; y: number } + time?: number | string + seriesData: Map, ToucanChartData> +} + +/** + * Creates chart configuration options + * Extracted as a pure function for testability + */ +function createChartOptions(params: { + width: number + height: number + colors: UseSporeColorsReturn + priceFormatter: (price: number) => string +}) { + const { width, height, colors, priceFormatter } = params + return { + width, + height, + layout: { + background: { type: ColorType.Solid, color: 'transparent' }, + textColor: colors.neutral2.val, + fontSize: 10, + }, + grid: { + vertLines: { visible: false }, + horzLines: { color: colors.surface3Solid.val, style: LineStyle.SparseDotted }, + }, + leftPriceScale: { + visible: true, + borderVisible: false, // Hide y-axis border + textColor: colors.neutral2.val, + minimumWidth: 40, // Reduce left padding by setting a smaller minimum width for the y-axis + }, + rightPriceScale: { + visible: false, + }, + timeScale: { + visible: true, + borderVisible: true, // Show border (left line) as solid + borderColor: colors.surface3Solid.val, // Same color as grid lines + fixLeftEdge: true, + fixRightEdge: true, + lockVisibleTimeRangeOnResize: true, + rightBarStaysOnScroll: false, + timeVisible: false, + secondsVisible: false, + tickMarkFormatter: () => '', + }, + crosshair: { + horzLine: { + visible: true, + style: LineStyle.Dashed, + width: 1 as const, + color: colors.neutral3.val, + labelVisible: true, + }, + vertLine: { + visible: true, + style: LineStyle.Dashed, + width: 1 as const, + color: colors.neutral3.val, + labelVisible: false, + }, + }, + handleScroll: { + mouseWheel: true, + pressedMouseMove: true, + horzTouchDrag: true, + vertTouchDrag: false, + }, + handleScale: { + mouseWheel: true, + pinch: true, + axisPressedMouseMove: { + time: true, + price: false, + }, + }, + localization: { + priceFormatter, + }, + } +} + +interface BidDistributionChartRendererProps { + bidData: BidDistributionData + bidTokenInfo: BidTokenInfo + displayMode: DisplayMode + totalSupply?: string + auctionTokenDecimals?: number + clearingPrice: string + tickSize: string + tokenColor?: string + height?: number + preCalculatedConcentration: BidConcentrationResult | null +} + +export function BidDistributionChartRenderer({ + bidData, + bidTokenInfo, + displayMode, + totalSupply, + auctionTokenDecimals = 18, + clearingPrice, + tickSize, + tokenColor, + height = 400, + preCalculatedConcentration, +}: BidDistributionChartRendererProps) { + const auctionState = useAuctionStore((state) => state.progress.state) + const chartContainerRef = useRef(null) + const chartRef = useRef(null) + const seriesRef = useRef | null>(null) + const tooltipRef = useRef(null) + const labelsLayerRef = useRef(null) + const shouldAnimateRef = useRef(false) + + const colors = useSporeColors() + const { convertFiatAmountFormatted } = useLocalizationContext() + + // Get zoom state and actions from store + const chartZoomState = useAuctionStore((state) => state.chartZoomState) + const { setChartZoomState } = useAuctionStoreActions() + + // Y-axis formatter: Uses localization with currency symbol for bid amounts + const formatYAxisLabel = useCallback( + (amount: number): string => { + return convertFiatAmountFormatted(amount, NumberType.FiatTokenStats) + }, + [convertFiatAmountFormatted], + ) + + // X-axis formatter: No currency symbol, removes trailing zeros (e.g., $1.00 → 1, $1.50 → 1.5) + const formatXAxisLabel = useCallback( + (amount: number): string => { + // Use localization but without currency + const formatted = convertFiatAmountFormatted(amount, NumberType.FiatTokenStats) + + // Extract just the numeric part by removing non-numeric characters except . and , + const numericPart = formatted.replace(/[^\d.,\s]/g, '').trim() + + // Parse and re-format to remove unnecessary trailing zeros + const num = parseFloat(numericPart.replace(/,/g, '')) + if (isNaN(num)) { + return numericPart + } + + // Format with minimal decimal places, removing trailing zeros + return num.toString() + }, + [convertFiatAmountFormatted], + ) + + // Tooltip formatter: Includes currency symbol and K/M/B suffixes + const formatTooltipLabel = useCallback( + (amount: number): string => { + // For tokenPrice mode: show currency symbol (e.g., $1.50) + // For valuation mode: show with K/M/B suffix (e.g., $1.5M) + return convertFiatAmountFormatted(amount, NumberType.FiatTokenStats) + }, + [convertFiatAmountFormatted], + ) + + // Convert tick size to decimal for use throughout component + const tickSizeDecimal = useMemo( + () => Number(formatUnits(BigInt(tickSize), bidTokenInfo.decimals)), + [tickSize, bidTokenInfo.decimals], + ) + + // Convert clearing price to decimal + const clearingPriceDecimal = useMemo( + () => Number(formatUnits(BigInt(clearingPrice), bidTokenInfo.decimals)), + [clearingPrice, bidTokenInfo.decimals], + ) + + // Generate chart data (uses X-axis formatter for tick display strings) + // TODO | Toucan - Performance: generateChartData calculates concentration internally but we + // immediately override it with preCalculatedConcentration. Consider adding a skipConcentration + // flag to generateChartData to avoid duplicate sliding-window calculation. + const { bars, minTick, maxTick, labelIncrement, concentration } = useMemo(() => { + const chartData = generateChartData({ + bidData, + bidTokenInfo, + displayMode, + totalSupply, + auctionTokenDecimals, + clearingPrice, + tickSize, + formatter: formatXAxisLabel, + }) + + // Always use pre-calculated concentration from parent to ensure indices match across components + return { ...chartData, concentration: preCalculatedConcentration } + }, [ + bidData, + bidTokenInfo, + displayMode, + totalSupply, + auctionTokenDecimals, + clearingPrice, + tickSize, + formatXAxisLabel, + preCalculatedConcentration, + ]) + + const { getPlotDimensions } = useChartDimensions() + const { renderLabels } = useChartLabels({ + minTick, + maxTick, + labelIncrement, + tickSize: tickSizeDecimal, + displayMode, + bidTokenInfo, + totalSupply, + auctionTokenDecimals, + formatter: formatXAxisLabel, + colors, + }) + const { createTooltipElement, formatTooltipText } = useChartTooltip({ + displayMode, + bidTokenInfo, + totalSupply, + auctionTokenDecimals, + formatter: formatTooltipLabel, + volumeFormatter: formatYAxisLabel, // Use Y-axis formatter for volume (bid amounts) + colors, + }) + + // Store min/max time for axis calculations + const minTimeRef = useRef(null) + const maxTimeRef = useRef(null) + + /** + * Creates the labels layer DOM element + */ + const createLabelsLayer = useCallback((): HTMLDivElement => { + const labelsLayer = document.createElement('div') + Object.assign(labelsLayer.style, { + position: 'absolute', + left: '0', + bottom: `${LABEL_CONFIG.BOTTOM_POSITION}px`, + width: '100%', + height: `${LABEL_CONFIG.HEIGHT}px`, + pointerEvents: 'none', + display: 'flex', + alignItems: 'flex-end', + justifyContent: 'stretch', + overflow: 'hidden', // Clip labels at plot area boundaries + }) + return labelsLayer + }, []) + + /** + * Updates custom labels when chart changes + */ + const updateCustomLabels = useCallback(() => { + if (!labelsLayerRef.current || !chartRef.current) { + return + } + + // Update labels layer dimensions to match plot area (prevents label overflow) + const plotDimensions = getPlotDimensions(chartContainerRef.current, chartRef.current) + labelsLayerRef.current.style.left = `${plotDimensions.left}px` + labelsLayerRef.current.style.width = `${plotDimensions.width}px` + + // Since labels layer now starts at plotLeft, individual labels should be positioned + // relative to the layer (at x) rather than relative to the container (x + plotLeft) + renderLabels({ labelsLayer: labelsLayerRef.current, chart: chartRef.current, plotLeft: 0 }) + }, [getPlotDimensions, renderLabels]) + + /** + * Formats the clearing price label for display on the chart + * Uses tooltip formatter to include currency symbols and FDV suffix + */ + const clearingPriceLabel = useMemo(() => { + return formatClearingPriceLabel({ + clearingPrice: clearingPriceDecimal, + displayMode, + bidTokenInfo, + totalSupply, + auctionTokenDecimals, + formatter: formatTooltipLabel, + }) + }, [clearingPriceDecimal, displayMode, bidTokenInfo, totalSupply, auctionTokenDecimals, formatTooltipLabel]) + + /** + * Converts bars to histogram data format + */ + const histogramData = useMemo((): ToucanChartData[] => { + return bars.map((bar) => ({ + time: Math.round(bar.tick * COORDINATE_SCALING.PRICE_SCALE_FACTOR) as UTCTimestamp, + value: bar.amount, + tickValue: bar.tick, // Store original tick value for color comparison + })) + }, [bars]) + + // TODO | Toucan -- this useEffect has too many responsibilities + // Once final functionality is decided on, refactor to smaller single responsibility code blocks + // biome-ignore lint/correctness/useExhaustiveDependencies: Only include dependencies that should cause a full chart recreation. + useEffect(() => { + if (!chartContainerRef.current) { + return undefined + } + + let chart: IChartApi + try { + const chartOptions = createChartOptions({ + width: chartContainerRef.current.clientWidth, + height, + colors, + priceFormatter: formatYAxisLabel, + }) + chart = createChart(chartContainerRef.current, chartOptions) + } catch (error) { + logger.error(error, { + tags: { + file: 'BidDistributionChartRenderer', + function: 'BidDistributionChartRenderer', + }, + }) + return () => { + // Ensure animation is stopped even if chart creation failed + shouldAnimateRef.current = false + } + } + + // Prepare bar colors with tokenColor or fallback to colors.accent1.val + const effectiveTokenColor = tokenColor || colors.accent1.val + const barColors = { + clearingPriceColor: effectiveTokenColor, + aboveClearingPriceColor: opacify(40, effectiveTokenColor), + belowClearingPriceColor: colors.neutral3.val, + } + + // Prepare label colors for clearing price label (supports light/dark themes) + const labelColors = { + background: colors.surface2.val, + border: colors.surface3.val, + text: colors.neutral1.val, + } + + // Prepare label styles for clearing price label + // Note: Canvas rendering requires a CSS font-family string. The theme only provides + // `fonts.code` for monospace, but we need the default system font stack for UI text. + // This matches the standard web font fallback stack used throughout the app. + const labelStyles = { + fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', + } + + // Create custom series with concentration band info and clearing price label + const customSeries = chart.addCustomSeries( + new ToucanChartSeries({ + barColors, + labelColors, + labelStyles, + clearingPrice: clearingPriceDecimal, + tickSize: tickSizeDecimal, + concentrationBand: concentration + ? { + startIndex: concentration.startIndex, + endIndex: concentration.endIndex, + startTick: concentration.startTick, + endTick: concentration.endTick, + } + : null, + clearingPriceLabel, + }), + { + priceScaleId: 'left', + priceLineVisible: false, // Hide the default price line + lastValueVisible: false, // Hide the last value label + }, + ) + + customSeries.setData(histogramData) + + // Store min/max price (in scaled coordinate units) for axis range calculations + // Note: Using 'time' terminology due to lightweight-charts API, but these represent prices + if (histogramData.length > 0) { + minTimeRef.current = histogramData[0].time + maxTimeRef.current = histogramData[histogramData.length - 1].time + } + + // Create and append labels layer + const labelsLayer = createLabelsLayer() + chartContainerRef.current.appendChild(labelsLayer) + labelsLayerRef.current = labelsLayer + + // Create and append tooltip + const tooltip = createTooltipElement() + chartContainerRef.current.style.position = 'relative' + chartContainerRef.current.appendChild(tooltip) + tooltipRef.current = tooltip + + // TODO | Toucan - extract crosshair to separate function + // Crosshair handler for tooltip + const crosshairHandler = (param: unknown) => { + if (!tooltipRef.current) { + return + } + const tooltip = tooltipRef.current + + // Type guard to ensure param has expected structure + if (!param || typeof param !== 'object') { + tooltip.style.display = 'none' + return + } + + const typedParam = param as CrosshairMoveParams + const point = typedParam.point + + if (!point || !typedParam.time) { + tooltip.style.display = 'none' + return + } + + const data = typedParam.seriesData.get(customSeries) + if (!data) { + tooltip.style.display = 'none' + return + } + + // Convert time (scaled units) back to price value and format + const tickValue = Number(data.time) / COORDINATE_SCALING.PRICE_SCALE_FACTOR + const volumeAmount = data.value // Bid volume in USD + + // Hide horizontal crosshair line (y-axis indicator) for zero values + chart.applyOptions({ + crosshair: { + horzLine: { + labelVisible: volumeAmount > 0, + }, + }, + }) + + tooltip.textContent = formatTooltipText(tickValue, volumeAmount) + + // Calculate tooltip position with edge detection + if (!chartContainerRef.current) { + return + } + + const containerRect = chartContainerRef.current.getBoundingClientRect() + const tooltipRect = tooltip.getBoundingClientRect() + + // Adjust horizontal position if tooltip would be cut off + let adjustedX = point.x + const halfTooltipWidth = tooltipRect.width / 2 + + if (point.x + halfTooltipWidth > containerRect.width) { + // Too close to right edge + adjustedX = containerRect.width - halfTooltipWidth + } else if (point.x - halfTooltipWidth < 0) { + // Too close to left edge + adjustedX = halfTooltipWidth + } + + // Adjust vertical position if tooltip would be cut off + let adjustedY = point.y + const tooltipHeight = tooltipRect.height + + if (point.y - tooltipHeight < 0) { + // Too close to top edge + adjustedY = tooltipHeight + } else if (point.y > containerRect.height) { + // Too close to bottom edge + adjustedY = containerRect.height + } + + tooltip.style.left = `${adjustedX}px` + tooltip.style.top = `${adjustedY}px` + tooltip.style.display = 'block' + } + chart.subscribeCrosshairMove(crosshairHandler) + + // Configure Y-axis (bid amounts in USD) + // Note: In lightweight-charts, priceScale() always controls the Y-axis + chart.priceScale('left').applyOptions({ + scaleMargins: { + top: CHART_SCALE_MARGINS.TOP, + bottom: CHART_SCALE_MARGINS.BOTTOM, + }, + autoScale: true, // Automatically scale to fit data + }) + + // Configure X-axis visible range (token prices in scaled coordinate units) + // Note: In lightweight-charts, timeScale() always controls the X-axis, even though + // we're displaying prices (not time). This is a library architecture constraint. + + // Calculate initial visible range: clearing price + INITIAL_TICK_COUNT ticks + const initialRange = calculateInitialVisibleRange({ + clearingPrice: clearingPriceDecimal, + minTick, + maxTick, + tickSize: tickSizeDecimal, + initialTickCount: ZOOM_DEFAULTS.INITIAL_TICK_COUNT, + }) + + // Convert initial range to scaled coordinates + const initialFromScaled = Math.round(initialRange.from * COORDINATE_SCALING.PRICE_SCALE_FACTOR) as UTCTimestamp + const initialToScaled = Math.round(initialRange.to * COORDINATE_SCALING.PRICE_SCALE_FACTOR) as UTCTimestamp + + // Apply stored zoom state if it exists, otherwise use initial range + // TODO | Toucan - determine if zoom should be reset in certain cases (like when new data comes in, or always retain saved zoom level) + if (chartZoomState.visibleRange && minTimeRef.current !== null && maxTimeRef.current !== null) { + // Use stored zoom state + chart.timeScale().setVisibleRange({ + from: chartZoomState.visibleRange.from as UTCTimestamp, + to: chartZoomState.visibleRange.to as UTCTimestamp, + }) + } else if (minTimeRef.current !== null && maxTimeRef.current !== null) { + // Use initial calculated range + chart.timeScale().setVisibleRange({ + from: initialFromScaled, + to: initialToScaled, + }) + } + + // Initial render of labels (defer to next frame to ensure chart is fully laid out) + requestAnimationFrame(() => { + updateCustomLabels() + }) + + chartRef.current = chart + seriesRef.current = customSeries + + // Handle resize + const handleResize = () => { + if (chartContainerRef.current) { + chart.applyOptions({ + width: chartContainerRef.current.clientWidth, + }) + updateCustomLabels() + } + } + + // Subscribe to range changes for label updates + const visibleRangeHandler = () => updateCustomLabels() + chart.timeScale().subscribeVisibleTimeRangeChange(visibleRangeHandler) + const logicalRangeHandler = () => updateCustomLabels() + chart.timeScale().subscribeVisibleLogicalRangeChange(logicalRangeHandler) + + // Subscribe to visible range changes for zoom state tracking + const zoomStateHandler = () => { + const currentRange = chart.timeScale().getVisibleRange() + if (!currentRange || minTimeRef.current === null || maxTimeRef.current === null) { + return + } + + let from = currentRange.from as number + let to = currentRange.to as number + + // Calculate the full data range with padding (what "reset zoom" would show) + const fullFrom = minTimeRef.current - CHART_PADDING.RANGE_PAD_UNITS + const fullTo = maxTimeRef.current + CHART_PADDING.RANGE_PAD_UNITS + const currentRangeSize = to - from + + // Constrain visible range to data boundaries + // Don't allow scrolling past the min/max - the edges must stay at or beyond the data bounds + let needsCorrection = false + + // If trying to scroll left past the minimum + if (from < fullFrom) { + from = fullFrom + to = from + currentRangeSize + needsCorrection = true + } + + // If trying to scroll right past the maximum + if (to > fullTo) { + to = fullTo + from = to - currentRangeSize + needsCorrection = true + } + + // Double-check: if the range is now too large (showing more than full data), clamp it + if (from < fullFrom) { + from = fullFrom + needsCorrection = true + } + + // If we had to correct the range, apply it + if (needsCorrection) { + chart.timeScale().setVisibleRange({ + from: from as UTCTimestamp, + to: to as UTCTimestamp, + }) + return // Will trigger another call with corrected range + } + + // Calculate the range of the full data + const fullRange = fullTo - fullFrom + + // Determine if user is zoomed in (current range is significantly smaller than full range) + // Or if the visible range doesn't match the full range + const isZoomedIn = + Math.abs(from - fullFrom) > fullRange * ZOOM_TOLERANCE || + Math.abs(to - fullTo) > fullRange * ZOOM_TOLERANCE || + Math.abs(currentRangeSize - fullRange) > fullRange * ZOOM_TOLERANCE + + // Update store with new zoom state + setChartZoomState({ + visibleRange: { from, to }, + isZoomed: isZoomedIn, + }) + } + chart.timeScale().subscribeVisibleTimeRangeChange(zoomStateHandler) + + window.addEventListener('resize', handleResize) + + // Animation loop for clearing price stripes + let animationFrameId: number | null = null + shouldAnimateRef.current = true + + const animate = () => { + // Check if animation should continue + if (!shouldAnimateRef.current) { + animationFrameId = null + return + } + + if (chartRef.current && seriesRef.current) { + // Trigger redraw by updating the series + customSeries.applyOptions({}) + } + animationFrameId = requestAnimationFrame(animate) + } + + // Start animation + animationFrameId = requestAnimationFrame(animate) + + // Cleanup + return () => { + // Signal animation to stop + shouldAnimateRef.current = false + + if (animationFrameId !== null) { + cancelAnimationFrame(animationFrameId) + animationFrameId = null + } + + window.removeEventListener('resize', handleResize) + chart.timeScale().unsubscribeVisibleTimeRangeChange(visibleRangeHandler) + chart.timeScale().unsubscribeVisibleLogicalRangeChange(logicalRangeHandler) + chart.timeScale().unsubscribeVisibleTimeRangeChange(zoomStateHandler) + chart.unsubscribeCrosshairMove(crosshairHandler) + + if (tooltipRef.current && chartContainerRef.current?.contains(tooltipRef.current)) { + chartContainerRef.current.removeChild(tooltipRef.current) + tooltipRef.current = null + } + + if (labelsLayerRef.current && chartContainerRef.current?.contains(labelsLayerRef.current)) { + chartContainerRef.current.removeChild(labelsLayerRef.current) + labelsLayerRef.current = null + } + + chart.remove() + chartRef.current = null + seriesRef.current = null + } + }, [ + height, + tokenColor, + histogramData, + concentration, + clearingPriceDecimal, + tickSizeDecimal, + formatYAxisLabel, + createTooltipElement, + formatTooltipText, + createLabelsLayer, + updateCustomLabels, + colors.neutral2.val, + colors.neutral3.val, + colors.surface3Solid.val, + colors.accent1.val, + clearingPriceLabel, + ]) + + // Handle zoom reset: when user clicks reset button, show full data range + useEffect(() => { + if (!chartRef.current || minTimeRef.current === null || maxTimeRef.current === null) { + return + } + + // If zoom state is reset (visibleRange is null and isZoomed is false), + // and we're not in the initial render, apply full data range + if (!chartZoomState.isZoomed && chartZoomState.visibleRange === null) { + // Apply full data range with padding + chartRef.current.timeScale().setVisibleRange({ + from: (minTimeRef.current - CHART_PADDING.RANGE_PAD_UNITS) as UTCTimestamp, + to: (maxTimeRef.current + CHART_PADDING.RANGE_PAD_UNITS) as UTCTimestamp, + }) + } + }, [chartZoomState.isZoomed, chartZoomState.visibleRange]) + + // Show placeholder when auction hasn't started + if (auctionState === AuctionProgressState.NOT_STARTED) { + return + } + + return ( + + + + ) +} diff --git a/apps/web/src/components/Toucan/Auction/BidDistributionChart/BlockUpdateCountdown.tsx b/apps/web/src/components/Toucan/Auction/BidDistributionChart/BlockUpdateCountdown.tsx new file mode 100644 index 00000000000..de6d7f12122 --- /dev/null +++ b/apps/web/src/components/Toucan/Auction/BidDistributionChart/BlockUpdateCountdown.tsx @@ -0,0 +1,23 @@ +import { useBlockCountdown } from 'hooks/useBlockCountdown' +import { useTranslation } from 'react-i18next' +import { Text } from 'ui/src' +import { EVMUniverseChainId } from 'uniswap/src/features/chains/types' + +interface BlockUpdateCountdownProps { + chainId: EVMUniverseChainId | undefined +} + +export const BlockUpdateCountdown = ({ chainId }: BlockUpdateCountdownProps) => { + const { t } = useTranslation() + const countdown = useBlockCountdown(chainId) + + if (countdown === undefined) { + return null + } + + return ( + + {t('toucan.auction.nextBlockUpdate', { seconds: Math.ceil(countdown) })} + + ) +} diff --git a/apps/web/src/components/Toucan/Auction/BidDistributionChart/constants.ts b/apps/web/src/components/Toucan/Auction/BidDistributionChart/constants.ts new file mode 100644 index 00000000000..96b02335579 --- /dev/null +++ b/apps/web/src/components/Toucan/Auction/BidDistributionChart/constants.ts @@ -0,0 +1,92 @@ +// Chart dimensions and spacing +export const CHART_PADDING = { + RANGE_PAD_UNITS: 25, // Padding in scaled coordinate units for visible range +} as const + +// Price coordinate scaling +export const COORDINATE_SCALING = { + PRICE_SCALE_FACTOR: 10000, // Scale factor for converting prices to integer coordinates (supports precision to 0.0001) +} as const + +// Label configuration +export const LABEL_CONFIG = { + FONT_SIZE: 10, + LINE_HEIGHT: 10, + BOTTOM_POSITION: -2, // Distance from bottom of chart (negative moves labels down/away from chart) + HEIGHT: 16, // Height of labels layer +} as const + +// Tooltip configuration +export const TOOLTIP_CONFIG = { + PADDING: '4px 6px', + BORDER_RADIUS: '6px', + FONT_SIZE: 12, + OFFSET_X: 10, // Horizontal offset from cursor + VERTICAL_OFFSET_PERCENT: 120, // Vertical offset as percentage +} as const + +// Clearing price line configuration +export const CLEARING_PRICE_LINE = { + WIDTH: 2, // Line width in pixels + DASH_PATTERN: [6, 4], // Dash pattern for the line (6px dash, 4px gap) + LABEL_OFFSET_Y: 12, // Vertical offset for the label from top of chart +} as const + +// Chart scale margins +export const CHART_SCALE_MARGINS = { + TOP: 0.2, + BOTTOM: 0, +} as const + +// Bar styling +export const BAR_STYLE = { + BORDER_RADIUS: 12, // Corresponds to $rounded12 + SPACING: 2, // Gap between adjacent bars in pixels +} as const + +// TODO | Toucan: Make gradient dynamic based on token color +// The gradient should be calculated from the token's brand color (obtained via getRGBColor +// from the token's image URL). For the test token, this resolves to #7482FF from +// specialCaseTokens.ts. The START_COLOR should use the token color with opacity (e.g., 32%), +// and END_COLOR should be a darker variant with 0% opacity. This will require passing the +// token color to the renderer and dynamically generating the gradient colors. +// Concentration gradient styling +export const CONCENTRATION_GRADIENT = { + START_COLOR: 'rgba(127, 124, 251, 0.32)', // Bottom of gradient (visible) + END_COLOR: 'rgba(75, 74, 149, 0)', // Top of gradient (transparent) +} as const + +// Zoom configuration +export const ZOOM_DEFAULTS = { + INITIAL_TICK_COUNT: 20, // Number of ticks to show in initial view +} as const + +export const ZOOM_TOLERANCE = 0.01 // Tolerance for detecting "full zoom" state (1%) + +// Floating point comparison tolerances +export const TOLERANCE = { + TICK_COMPARISON: 0.001, // Tolerance for tick value comparisons (0.1% of tick size) + TICK_MATCHING: 0.1, // Tolerance for matching ticks in concentration band (10% of tick size) + FALLBACK: 0.01, // Fallback tolerance when tick size is unavailable +} as const + +// Chart data constraints +export const CHART_CONSTRAINTS = { + MIN_BARS: 20, // Minimum number of bars to display +} as const + +// Label generation configuration +export const LABEL_GENERATION = { + MIN_LABELS: 7, // Minimum number of x-axis labels + MAX_LABELS: 12, // Maximum number of x-axis labels + IDEAL_LABELS: 10, // Ideal target number of labels +} as const + +// Nice number values for rounding and label increments +export const NICE_VALUES = { + STANDARD: [1, 2, 5] as const, // Standard nice values for chart increments + WITH_HALF: [1, 2, 2.5, 5] as const, // Includes 2.5 for finer granularity (Y-axis) +} as const + +// Default Y-axis levels for empty charts +export const DEFAULT_Y_AXIS_LEVELS = [0, 20000, 40000, 60000, 80000, 100000] as const diff --git a/apps/web/src/components/Toucan/Auction/BidDistributionChart/dev/CustomizePresetForm.tsx b/apps/web/src/components/Toucan/Auction/BidDistributionChart/dev/CustomizePresetForm.tsx new file mode 100644 index 00000000000..091d29b3862 --- /dev/null +++ b/apps/web/src/components/Toucan/Auction/BidDistributionChart/dev/CustomizePresetForm.tsx @@ -0,0 +1,472 @@ +// TODO: Remove this file once live auction data is implemented +/** biome-ignore-all lint/correctness/useExhaustiveDependencies: dev-only file with intentional dependencies */ +// Form for customizing and generating bid distribution presets + +import { + AUCTION_TOKEN_DECIMALS, + BID_TOKEN_CONFIGS, + CustomPresetParams, + fromHumanReadable, + generatePresetName, + generateRandomBidDistribution, + SavedCustomPreset, + toHumanReadable, + validateTickCount, +} from 'components/Toucan/Auction/BidDistributionChart/dev/customPresets' +import { useCustomPresetsStore } from 'components/Toucan/Auction/BidDistributionChart/dev/useCustomPresetsStore' +import { useMockDataStore } from 'components/Toucan/Auction/store/mocks/useMockDataStore' +import { useCallback, useEffect, useState } from 'react' +import { Button, Flex, Input, SegmentedControl, Text } from 'ui/src' + +interface CustomizePresetFormProps { + onClose: () => void + editingPreset?: SavedCustomPreset | null +} + +export const CustomizePresetForm = ({ onClose, editingPreset }: CustomizePresetFormProps) => { + const savePreset = useCustomPresetsStore((state) => state.savePreset) + const updatePreset = useCustomPresetsStore((state) => state.updatePreset) + const loadCustomPreset = useMockDataStore((state) => state.loadCustomPreset) + + const isEditMode = !!editingPreset + + // Form state + const [bidToken, setBidToken] = useState<'USDC' | 'ETH'>('USDC') + const [tickSizeHuman, setTickSizeHuman] = useState(BID_TOKEN_CONFIGS.USDC.defaultTickSizeHuman) + const [clearingPriceHuman, setClearingPriceHuman] = useState(BID_TOKEN_CONFIGS.USDC.defaultClearingPriceHuman) + const [tickRangeMin, setTickRangeMin] = useState('1') + const [tickRangeMax, setTickRangeMax] = useState('100') + const [tickCount, setTickCount] = useState('20') + const [bidVolumeMinHuman, setBidVolumeMinHuman] = useState('1000') + const [bidVolumeMaxHuman, setBidVolumeMaxHuman] = useState('10000') + const [totalSupply, setTotalSupply] = useState('1000000000') + + // Load editing preset values + useEffect(() => { + if (editingPreset) { + const config = BID_TOKEN_CONFIGS[editingPreset.bidToken] + setBidToken(editingPreset.bidToken) + setTickSizeHuman(toHumanReadable(editingPreset.tickSize, config.decimals)) + setClearingPriceHuman(toHumanReadable(editingPreset.clearingPrice, config.decimals)) + setTickRangeMin(editingPreset.tickRangeMin.toString()) + setTickRangeMax(editingPreset.tickRangeMax.toString()) + setTickCount(editingPreset.tickCount.toString()) + setBidVolumeMinHuman(toHumanReadable(editingPreset.bidVolumeMin, config.decimals)) + setBidVolumeMaxHuman(toHumanReadable(editingPreset.bidVolumeMax, config.decimals)) + // Convert totalSupply from raw (with 18 decimals) to human-readable + setTotalSupply(toHumanReadable(editingPreset.totalSupply, AUCTION_TOKEN_DECIMALS)) + // Note: bidTokenAddress is derived from bidToken config, not loaded separately + } + }, [editingPreset]) + + const [error, setError] = useState('') + + const config = BID_TOKEN_CONFIGS[bidToken] + + // Handle bid token change + const handleBidTokenChange = useCallback((value: string) => { + const newToken = value as 'USDC' | 'ETH' + setBidToken(newToken) + const newConfig = BID_TOKEN_CONFIGS[newToken] + setTickSizeHuman(newConfig.defaultTickSizeHuman) + setClearingPriceHuman(newConfig.defaultClearingPriceHuman) + setError('') + }, []) + + // Validate clearing price on blur + const handleClearingPriceBlur = useCallback(() => { + try { + const clearingPriceRaw = fromHumanReadable(clearingPriceHuman, config.decimals) + if (BigInt(clearingPriceRaw) < 0n) { + setError('Invalid clearing price') + } else { + setError('') + } + } catch { + setError('Invalid clearing price') + } + }, [clearingPriceHuman, config.decimals]) + + // Arrow key handlers for numeric inputs + const handleTickSizeKeyPress = useCallback( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e: any) => { + if (e.key === 'ArrowUp' || e.key === 'ArrowDown') { + e.preventDefault() + const current = parseFloat(tickSizeHuman || '0') + const increment = e.key === 'ArrowUp' ? 0.01 : -0.01 + const newValue = Math.max(0.01, current + increment) + setTickSizeHuman(newValue.toFixed(2)) + } + }, + [tickSizeHuman], + ) + + const handleClearingPriceKeyPress = useCallback( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e: any) => { + if (e.key === 'ArrowUp' || e.key === 'ArrowDown') { + e.preventDefault() + const clearingPriceRaw = fromHumanReadable(clearingPriceHuman, config.decimals) + const tickSizeRaw = fromHumanReadable(tickSizeHuman, config.decimals) + const tickSizeBigInt = BigInt(tickSizeRaw) + const increment = e.key === 'ArrowUp' ? tickSizeBigInt : -tickSizeBigInt + const newValue = BigInt(clearingPriceRaw) + increment + const newValueHuman = toHumanReadable(newValue.toString(), config.decimals) + setClearingPriceHuman(newValueHuman) + } + }, + [clearingPriceHuman, tickSizeHuman, config.decimals], + ) + + const handleNumericKeyPress = useCallback( + (setter: (value: string) => void, currentValue: string) => + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e: any) => { + if (e.key === 'ArrowUp' || e.key === 'ArrowDown') { + e.preventDefault() + const current = parseInt(currentValue || '0') + const increment = e.key === 'ArrowUp' ? 1 : -1 + const newValue = Math.max(0, current + increment) + setter(newValue.toString()) + } + }, + [], + ) + + // Validate and save preset + const handleSave = useCallback(() => { + try { + setError('') + + // Parse inputs + const tickSizeRaw = fromHumanReadable(tickSizeHuman, config.decimals) + const clearingPriceRaw = fromHumanReadable(clearingPriceHuman, config.decimals) + const rangeMin = parseInt(tickRangeMin) + const rangeMax = parseInt(tickRangeMax) + const count = parseInt(tickCount) + const volumeMinRaw = fromHumanReadable(bidVolumeMinHuman, config.decimals) + const volumeMaxRaw = fromHumanReadable(bidVolumeMaxHuman, config.decimals) + + // Validation + if (rangeMin < 1 || rangeMax > 40000 || rangeMin >= rangeMax) { + setError('Invalid tick range. Min must be 1-40000 and less than max.') + return + } + + if (!validateTickCount({ tickCount: count, tickRangeMin: rangeMin, tickRangeMax: rangeMax })) { + setError(`Tick count must be between 1 and ${rangeMax - rangeMin + 1}`) + return + } + + if (BigInt(volumeMinRaw) >= BigInt(volumeMaxRaw)) { + setError('Bid volume min must be less than max') + return + } + + // Generate preset parameters + // Convert totalSupply to raw format (with 18 decimals) + const totalSupplyRaw = fromHumanReadable(totalSupply, AUCTION_TOKEN_DECIMALS) + + const params: CustomPresetParams = { + bidToken, + bidTokenAddress: config.address, // Store actual token address + tickSize: tickSizeRaw, + clearingPrice: clearingPriceRaw, + tickRangeMin: rangeMin, + tickRangeMax: rangeMax, + tickCount: count, + bidVolumeMin: volumeMinRaw, + bidVolumeMax: volumeMaxRaw, + totalSupply: totalSupplyRaw, + } + + // Generate distribution data + const distributionData = generateRandomBidDistribution(params) + const name = generatePresetName(params) + + if (isEditMode) { + // Update existing preset + updatePreset(editingPreset.id, { + ...params, + name, + distributionData, + }) + + // Load the updated preset + const updatedPreset = { + ...params, + name, + distributionData, + id: editingPreset.id, + createdAt: editingPreset.createdAt, + } + loadCustomPreset(updatedPreset) + } else { + // Save new preset + savePreset({ + ...params, + name, + distributionData, + }) + + // Load the preset (need to create a temporary one with id for loading) + const tempPreset = { + ...params, + name, + distributionData, + id: 'temp', // Will be updated when we select from saved list + createdAt: Date.now(), + } + loadCustomPreset(tempPreset) + } + + onClose() + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to generate preset') + } + }, [ + bidToken, + tickSizeHuman, + clearingPriceHuman, + tickRangeMin, + tickRangeMax, + tickCount, + bidVolumeMinHuman, + bidVolumeMaxHuman, + config.decimals, + savePreset, + loadCustomPreset, + onClose, + ]) + + return ( + + {/* Bid Token Selector */} + + + Bid Token + + USDC }, + { value: 'ETH', display: ETH }, + ]} + selectedOption={bidToken} + onSelectOption={handleBidTokenChange} + /> + + + {/* Tick Size and Clearing Price - Same Row */} + + + + Tick Size ({bidToken}) + + + + + + + + + Clearing Price ({bidToken}) + + + + Can be any price + + + + + {/* Tick Range - Same Row */} + + + + Tick Range (multipliers: 1-40000) + + + + + to + + + + + + + + + + Ticks with Bids + + + + Max: {Math.max(0, parseInt(tickRangeMax || '0') - parseInt(tickRangeMin || '0') + 1)} + + + + + {/* Bid Volume Range and Total Supply - Same Row */} + + + + Bid Volume Range (per tick, in {bidToken}) + + + + + to + + + + + + + + + + Total Supply + + + + + + {/* Error Message */} + {error && ( + + {error} + + )} + + {/* Preview */} + + + Preview: + + + {tickCount} random ticks between $ + {toHumanReadable( + (BigInt(fromHumanReadable(tickSizeHuman, config.decimals)) * BigInt(tickRangeMin || '1')).toString(), + config.decimals, + )}{' '} + - $ + {toHumanReadable( + (BigInt(fromHumanReadable(tickSizeHuman, config.decimals)) * BigInt(tickRangeMax || '100')).toString(), + config.decimals, + )} + + + Volume per tick: ${bidVolumeMinHuman} - ${bidVolumeMaxHuman} + + + + {/* Save/Update Button */} + + + ) +} diff --git a/apps/web/src/components/Toucan/Auction/BidDistributionChart/dev/MockDataSelectorModal.tsx b/apps/web/src/components/Toucan/Auction/BidDistributionChart/dev/MockDataSelectorModal.tsx new file mode 100644 index 00000000000..9fbfaf2c468 --- /dev/null +++ b/apps/web/src/components/Toucan/Auction/BidDistributionChart/dev/MockDataSelectorModal.tsx @@ -0,0 +1,139 @@ +// TODO: Remove this file once live auction data is implemented +// Modal for selecting mock bid distribution data in development + +import { CustomizePresetForm } from 'components/Toucan/Auction/BidDistributionChart/dev/CustomizePresetForm' +import { SavedCustomPreset } from 'components/Toucan/Auction/BidDistributionChart/dev/customPresets' +import { getDatasetLabel } from 'components/Toucan/Auction/BidDistributionChart/dev/devUtils' +import { SavedPresetsList } from 'components/Toucan/Auction/BidDistributionChart/dev/SavedPresetsList' +import { MOCK_BID_DISTRIBUTION_DATASETS } from 'components/Toucan/Auction/store/mocks/distributionData/bidDistributionMockData' +import { useMockDataStore } from 'components/Toucan/Auction/store/mocks/useMockDataStore' +import { BidTokenInfo } from 'components/Toucan/Auction/store/types' +import { useMemo, useState } from 'react' +import { Flex, SegmentedControl, Text, TouchableArea } from 'ui/src' +import { Modal } from 'uniswap/src/components/modals/Modal' +import { ModalName } from 'uniswap/src/features/telemetry/constants' + +type TabType = 'quick' | 'customize' | 'saved' + +interface MockDataSelectorModalProps { + bidTokenInfo: BidTokenInfo +} + +export const MockDataSelectorModal = ({ bidTokenInfo }: MockDataSelectorModalProps) => { + // Quick select datasets are all USDC-based, so use hardcoded USDC info for labels + // This prevents display issues when an ETH preset is selected as active + const quickSelectBidTokenInfo: BidTokenInfo = { + symbol: 'USDC', + decimals: 6, + priceFiat: 1, + } + const [isOpen, setIsOpen] = useState(false) + const [activeTab, setActiveTab] = useState('quick') + const [editingPreset, setEditingPreset] = useState(null) + const { selectedDatasetIndex, setSelectedDatasetIndex } = useMockDataStore() + + const datasetLabels = useMemo(() => { + return MOCK_BID_DISTRIBUTION_DATASETS.map((dataset) => getDatasetLabel(dataset, quickSelectBidTokenInfo)) + }, []) + + const handleSelectDataset = (index: number) => { + setSelectedDatasetIndex(index) + setIsOpen(false) + } + + const handleEditPreset = (preset: SavedCustomPreset) => { + setEditingPreset(preset) + setActiveTab('customize') + } + + const handleCloseModal = () => { + setIsOpen(false) + setEditingPreset(null) + // Reset to quick tab when closing + setTimeout(() => setActiveTab('quick'), 300) + } + + const handleCloseCustomizeForm = () => { + setEditingPreset(null) + setIsOpen(false) + // Reset to quick tab when closing + setTimeout(() => setActiveTab('quick'), 300) + } + + return ( + <> + {/* TODO | Toucan: Remove this dev button once live */} + setIsOpen(true)}> + + dev + + + + + + + Bid Distribution Data + + + {/* Tab Selector */} + Quick Select }, + { value: 'customize', display: Customize }, + { value: 'saved', display: Saved }, + ]} + selectedOption={activeTab} + onSelectOption={(value) => setActiveTab(value as TabType)} + /> + + {/* Tab Content */} + {activeTab === 'quick' && ( + + {MOCK_BID_DISTRIBUTION_DATASETS.map((dataset, index) => { + const { tickCount, minPrice, maxPrice } = datasetLabels[index] + const isSelected = selectedDatasetIndex === index + + return ( + handleSelectDataset(index)} + backgroundColor={isSelected ? '$surface3' : '$surface2'} + p="$spacing12" + borderRadius="$rounded12" + hoverStyle={{ backgroundColor: '$surface3' }} + > + + + {tickCount} Ticks + + + ${minPrice.toFixed(2)} - ${maxPrice.toFixed(2)} + + + + ) + })} + + )} + + {activeTab === 'customize' && ( + + )} + + {activeTab === 'saved' && } + + + + ) +} diff --git a/apps/web/src/components/Toucan/Auction/BidDistributionChart/dev/SavedPresetsList.tsx b/apps/web/src/components/Toucan/Auction/BidDistributionChart/dev/SavedPresetsList.tsx new file mode 100644 index 00000000000..af2dea9c6e8 --- /dev/null +++ b/apps/web/src/components/Toucan/Auction/BidDistributionChart/dev/SavedPresetsList.tsx @@ -0,0 +1,166 @@ +// TODO: Remove this file once live auction data is implemented +// List of saved custom bid distribution presets + +import { + BID_TOKEN_CONFIGS, + getBidTokenInfoFromConfig, + SavedCustomPreset, + toHumanReadable, +} from 'components/Toucan/Auction/BidDistributionChart/dev/customPresets' +import { getDatasetLabel } from 'components/Toucan/Auction/BidDistributionChart/dev/devUtils' +import { useCustomPresetsStore } from 'components/Toucan/Auction/BidDistributionChart/dev/useCustomPresetsStore' +import { useMockDataStore } from 'components/Toucan/Auction/store/mocks/useMockDataStore' +import { useState } from 'react' +import { Button, Flex, Text, TouchableArea } from 'ui/src' +import { Edit } from 'ui/src/components/icons/Edit' +import { Trash } from 'ui/src/components/icons/Trash' + +interface SavedPresetsListProps { + onClose: () => void + onEditPreset: (preset: SavedCustomPreset) => void +} + +export const SavedPresetsList = ({ onClose, onEditPreset }: SavedPresetsListProps) => { + const { presets, deletePreset, clearAllPresets } = useCustomPresetsStore() + const { loadCustomPreset, selectedPresetId } = useMockDataStore() + const [showConfirmClear, setShowConfirmClear] = useState(false) + + const handleLoadPreset = (presetId: string) => { + const preset = presets.find((p) => p.id === presetId) + if (preset) { + loadCustomPreset(preset) + onClose() + } + } + + const handleClearAll = () => { + if (showConfirmClear) { + clearAllPresets() + setShowConfirmClear(false) + } else { + setShowConfirmClear(true) + } + } + + // Empty state + if (presets.length === 0) { + return ( + + + No custom presets saved. + + + Create one in the Customize tab. + + + ) + } + + return ( + + + Saved Presets + + + + {presets.map((preset) => { + // Create preset-specific bidTokenInfo using its own config (not the active chart's token) + const presetBidTokenInfo = getBidTokenInfoFromConfig(preset.bidToken) + const label = getDatasetLabel(preset.distributionData, presetBidTokenInfo) + const config = BID_TOKEN_CONFIGS[preset.bidToken] + const isSelected = selectedPresetId === preset.id + + // Calculate volume range and clearing price for display + const minVolumeHuman = toHumanReadable(preset.bidVolumeMin, config.decimals) + const maxVolumeHuman = toHumanReadable(preset.bidVolumeMax, config.decimals) + const clearingPriceHuman = toHumanReadable(preset.clearingPrice, config.decimals) + + return ( + + handleLoadPreset(preset.id)} hoverStyle={{ opacity: 0.8 }}> + + + + {label.tickCount} Ticks ({config.symbol}) + + + ${label.minPrice.toFixed(2)} - ${label.maxPrice.toFixed(2)} + + + + {preset.name} + + + Volume per tick: ${minVolumeHuman} - ${maxVolumeHuman} + + + Clearing Price: ${clearingPriceHuman} + + + + + + onEditPreset(preset)} + p="$spacing8" + hoverStyle={{ backgroundColor: '$surface3' }} + borderRadius="$rounded8" + > + + + + deletePreset(preset.id)} + p="$spacing8" + hoverStyle={{ backgroundColor: '$surface3' }} + borderRadius="$rounded8" + > + + + + + ) + })} + + + {/* Clear All Button */} + + {showConfirmClear ? ( + + + Are you sure you want to delete all presets? + + + + + + + ) : ( + + )} + + + ) +} diff --git a/apps/web/src/components/Toucan/Auction/BidDistributionChart/dev/customPresets.ts b/apps/web/src/components/Toucan/Auction/BidDistributionChart/dev/customPresets.ts new file mode 100644 index 00000000000..a1e321809ac --- /dev/null +++ b/apps/web/src/components/Toucan/Auction/BidDistributionChart/dev/customPresets.ts @@ -0,0 +1,270 @@ +// TODO: Remove this file once live auction data is implemented +// Utilities for generating custom bid distribution presets + +import { BidDistributionData } from 'components/Toucan/Auction/store/types' + +// Auction token always has 18 decimals (matches FAKE_AUCTION_DATA.tokenDecimals) +export const AUCTION_TOKEN_DECIMALS = 18 + +// eslint-disable-next-line import/no-unused-modules -- Exported for type safety +export interface BidTokenConfig { + address: string + symbol: 'USDC' | 'ETH' + decimals: number + defaultTickSize: string // raw value (e.g., "500000" for 0.50 USDC) + defaultTickSizeHuman: string // human readable (e.g., "0.50") + defaultClearingPrice: string // raw value + defaultClearingPriceHuman: string // human readable +} + +export const BID_TOKEN_CONFIGS: Record<'USDC' | 'ETH', BidTokenConfig> = { + USDC: { + address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + symbol: 'USDC', + decimals: 6, + defaultTickSize: '500000', // 0.50 USDC + defaultTickSizeHuman: '0.50', + defaultClearingPrice: '5000000', // 5.00 USDC + defaultClearingPriceHuman: '5.00', + }, + ETH: { + address: '0x0000000000000000000000000000000000000000', // native ETH + symbol: 'ETH', + decimals: 18, + defaultTickSize: '100000000000000', // 0.0001 ETH + defaultTickSizeHuman: '0.0001', + defaultClearingPrice: '1000000000000000', // 0.001 ETH + defaultClearingPriceHuman: '0.001', + }, +} + +export interface CustomPresetParams { + bidToken: 'USDC' | 'ETH' + bidTokenAddress: string // actual token address (needed for useBidTokenInfo) + tickSize: string // raw value + clearingPrice: string // raw value + tickRangeMin: number // multiplier (e.g., 1 means 1x tickSize) + tickRangeMax: number // multiplier + tickCount: number + bidVolumeMin: string // raw value in bid token smallest units + bidVolumeMax: string // raw value in bid token smallest units + totalSupply: string // total supply of auction token +} + +export interface SavedCustomPreset extends CustomPresetParams { + id: string + name: string + createdAt: number + distributionData: BidDistributionData +} + +/** + * Generates random bid distribution data based on preset parameters + * Uses BigInt for precision-safe calculations + * Distribution is weighted to cluster near and above clearing price (like an order book) + */ +export function generateRandomBidDistribution(params: CustomPresetParams): BidDistributionData { + const { tickSize, clearingPrice, tickRangeMin, tickRangeMax, tickCount, bidVolumeMin, bidVolumeMax } = params + + // Calculate available ticks in the range + const availableTicks = tickRangeMax - tickRangeMin + 1 + + if (tickCount > availableTicks) { + throw new Error(`Tick count (${tickCount}) cannot exceed available range (${availableTicks})`) + } + + if (tickCount < 1) { + throw new Error('Tick count must be at least 1') + } + + const tickSizeBigInt = BigInt(tickSize) + const clearingPriceBigInt = BigInt(clearingPrice) + const volumeMinBigInt = BigInt(bidVolumeMin) + const volumeMaxBigInt = BigInt(bidVolumeMax) + const volumeRange = volumeMaxBigInt - volumeMinBigInt + + // Calculate which tick multiplier the clearing price falls at/near + const clearingMultiplier = Number(clearingPriceBigInt / tickSizeBigInt) + + // Generate random unique tick multipliers with clustering near clearing price + const tickMultipliers = new Set() + while (tickMultipliers.size < tickCount) { + let randomMultiplier: number + + // Decide if this should be below or at/above clearing price + // 70% of ticks should be at or above clearing price for realistic orderbook look + const isAboveOrAtClearing = Math.random() > 0.3 + + if (isAboveOrAtClearing) { + // Generate ticks at or above clearing price with tight clustering + // Use exponential distribution for realistic orderbook clustering + const maxDistance = tickRangeMax - clearingMultiplier + if (maxDistance > 0) { + // Very tight clustering: most ticks immediately near clearing + // Allow rare outliers (2% chance) + const isOutlier = Math.random() < 0.02 + let distance: number + + if (isOutlier) { + // Rare outlier: randomly distributed in range + distance = Math.floor(Math.random() * maxDistance) + } else { + // Normal: very tightly clustered near clearing price + // Use exponential with high lambda for tight clustering + const lambda = 8.0 // Higher = tighter clustering + const uniformRandom = Math.random() + const exponentialRandom = -Math.log(1 - uniformRandom) / lambda + // Scale to a smaller portion of maxDistance for tighter clustering + // Most ticks will be within first 10-20% of range + distance = Math.floor(Math.min(exponentialRandom, 0.3) * maxDistance) + } + + randomMultiplier = Math.min(tickRangeMax, Math.floor(clearingMultiplier + distance)) + } else { + randomMultiplier = Math.floor(clearingMultiplier) + } + } else { + // Generate ticks below clearing price with uniform random distribution + const minDistance = clearingMultiplier - tickRangeMin + if (minDistance > 0) { + const distance = Math.floor(Math.random() * minDistance) + randomMultiplier = Math.max(tickRangeMin, Math.floor(clearingMultiplier - distance) - 1) + } else { + randomMultiplier = tickRangeMin + } + } + + // Ensure within valid range + randomMultiplier = Math.max(tickRangeMin, Math.min(tickRangeMax, randomMultiplier)) + tickMultipliers.add(randomMultiplier) + } + + // Generate distribution data with volume that decreases with distance from clearing price + const distributionData = new Map() + + for (const multiplier of tickMultipliers) { + const tickValue = tickSizeBigInt * BigInt(multiplier) + const tickValueBigInt = BigInt(tickValue) + + // Calculate distance from clearing price for volume weighting + const distanceFromClearing = Math.abs(Number(tickValueBigInt - clearingPriceBigInt) / Number(tickSizeBigInt)) + + // For bids at or above clearing: volume decreases with distance (orderbook style) + // For bids below clearing: more random volume + let randomVolume: bigint + + if (tickValueBigInt >= clearingPriceBigInt) { + // At or above clearing: higher volume near clearing, exponentially decreasing + // Base volume factor: 1.0 at clearing, decreasing to ~0.3 at far distances + const distanceFactor = Math.exp(-distanceFromClearing * 0.05) + // Add some randomness (±30%) while maintaining general trend + const randomFactor = 0.7 + Math.random() * 0.6 // 0.7 to 1.3 + + const volumeFactor = distanceFactor * randomFactor + const scaledRange = Number(volumeRange) * volumeFactor + const randomOffset = BigInt(Math.floor(scaledRange * Math.random())) + const baseVolume = volumeMinBigInt + randomOffset + + // Ensure volume stays within bounds + randomVolume = baseVolume > volumeMaxBigInt ? volumeMaxBigInt : baseVolume + } else { + // Below clearing: use more random distribution + const randomRatio = Math.random() + const randomOffset = BigInt(Math.floor(Number(volumeRange) * randomRatio)) + randomVolume = volumeMinBigInt + randomOffset + } + + distributionData.set(tickValue.toString(), randomVolume.toString()) + } + + return distributionData +} + +/** + * Converts raw token value to human-readable decimal format + * @example toHumanReadable("500000", 6) // "0.5" + * @example toHumanReadable("100000000000000", 18) // "0.0001" + */ +export function toHumanReadable(value: string, decimals: number): string { + try { + const valueBigInt = BigInt(value) + const divisor = BigInt(Math.pow(10, decimals)) + + const wholePart = valueBigInt / divisor + const fractionalPart = valueBigInt % divisor + + if (fractionalPart === 0n) { + return wholePart.toString() + } + + const fractionalStr = fractionalPart.toString().padStart(decimals, '0') + // Remove trailing zeros + const trimmed = fractionalStr.replace(/0+$/, '') + + return `${wholePart}.${trimmed}` + } catch { + return '0' + } +} + +/** + * Converts human-readable decimal to raw token value + * @example fromHumanReadable("0.5", 6) // "500000" + * @example fromHumanReadable("0.0001", 18) // "100000000000000" + */ +export function fromHumanReadable(value: string, decimals: number): string { + try { + // Handle empty or invalid input + if (!value || value === '' || value === '.') { + return '0' + } + + const [whole = '0', fractional = ''] = value.split('.') + const paddedFractional = fractional.padEnd(decimals, '0').slice(0, decimals) + const combined = (whole === '' ? '0' : whole) + paddedFractional + return BigInt(combined || '0').toString() + } catch { + return '0' + } +} + +/** + * Validates that tick count doesn't exceed available range + */ +export function validateTickCount(params: { tickCount: number; tickRangeMin: number; tickRangeMax: number }): boolean { + const { tickCount, tickRangeMin, tickRangeMax } = params + const availableTicks = tickRangeMax - tickRangeMin + 1 + return tickCount >= 1 && tickCount <= availableTicks +} + +/** + * Generates a descriptive name for a custom preset + */ +export function generatePresetName(params: CustomPresetParams): string { + const config = BID_TOKEN_CONFIGS[params.bidToken] + const minTick = toHumanReadable((BigInt(params.tickSize) * BigInt(params.tickRangeMin)).toString(), config.decimals) + const maxTick = toHumanReadable((BigInt(params.tickSize) * BigInt(params.tickRangeMax)).toString(), config.decimals) + + return `${params.bidToken} | ${params.tickCount} ticks | $${minTick}-$${maxTick}` +} + +/** + * Creates BidTokenInfo from a bid token config for display purposes + * Uses mock USD prices since this is only for displaying preset labels in the list + * The actual chart rendering uses real prices from useBidTokenInfo + */ +export function getBidTokenInfoFromConfig(bidToken: 'USDC' | 'ETH'): { + symbol: string + decimals: number + priceFiat: number +} { + const config = BID_TOKEN_CONFIGS[bidToken] + // Mock prices for display only - USDC is $1, ETH is $3000 + const mockPriceFiat = bidToken === 'USDC' ? 1 : 3000 + + return { + symbol: config.symbol, + decimals: config.decimals, + priceFiat: mockPriceFiat, + } +} diff --git a/apps/web/src/components/Toucan/Auction/BidDistributionChart/dev/devUtils.ts b/apps/web/src/components/Toucan/Auction/BidDistributionChart/dev/devUtils.ts new file mode 100644 index 00000000000..b635adfe62d --- /dev/null +++ b/apps/web/src/components/Toucan/Auction/BidDistributionChart/dev/devUtils.ts @@ -0,0 +1,59 @@ +// TODO: Remove this file once live auction data is implemented +// Utility functions for dev components + +import { BidDistributionData, BidTokenInfo } from 'components/Toucan/Auction/store/types' + +/** + * Generates a dynamic label for a mock dataset showing tick count and price range + * Converts tick values from smallest units (e.g., micro-USDC) to USD using bid token decimals and price + * Uses BigInt for precision-safe calculations with high-decimal tokens + */ +export function getDatasetLabel( + data: BidDistributionData, + bidTokenInfo: BidTokenInfo, +): { tickCount: number; minPrice: number; maxPrice: number } { + // Ensure data is a Map (handle deserialization edge cases) + let mapData: Map + if (data instanceof Map) { + mapData = data + } else if (Array.isArray(data)) { + mapData = new Map(data as [string, string][]) + } else { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- Runtime safety for deserialized data + mapData = new Map(data && typeof data === 'object' ? (Object.entries(data) as [string, string][]) : []) + } + + const tickCount = mapData.size + + // Handle empty data + if (tickCount === 0) { + return { + tickCount: 0, + minPrice: 0, + maxPrice: 0, + } + } + + const tickBigInts = Array.from(mapData.keys()).map((tick) => BigInt(tick)) + const minTickBigInt = tickBigInts.reduce((min, curr) => (curr < min ? curr : min)) + const maxTickBigInt = tickBigInts.reduce((max, curr) => (curr > max ? curr : max)) + + // Convert from smallest units to decimal using BigInt division + // We multiply by a scale factor first to preserve precision during division + // Use the token's decimals as the scale factor to maintain full precision + const SCALE_FACTOR = BigInt(Math.pow(10, bidTokenInfo.decimals)) + const decimalsDiv = SCALE_FACTOR + + const minPriceScaled = (minTickBigInt * SCALE_FACTOR) / decimalsDiv + const maxPriceScaled = (maxTickBigInt * SCALE_FACTOR) / decimalsDiv + + // Convert to USD (now safe to convert to Number since we're dealing with reasonable display values) + const minPrice = (Number(minPriceScaled) / Number(SCALE_FACTOR)) * bidTokenInfo.priceFiat + const maxPrice = (Number(maxPriceScaled) / Number(SCALE_FACTOR)) * bidTokenInfo.priceFiat + + return { + tickCount, + minPrice, + maxPrice, + } +} diff --git a/apps/web/src/components/Toucan/Auction/BidDistributionChart/dev/useCustomPresetsStore.ts b/apps/web/src/components/Toucan/Auction/BidDistributionChart/dev/useCustomPresetsStore.ts new file mode 100644 index 00000000000..de6b8ecc693 --- /dev/null +++ b/apps/web/src/components/Toucan/Auction/BidDistributionChart/dev/useCustomPresetsStore.ts @@ -0,0 +1,131 @@ +// TODO: Remove this file once live auction data is implemented +// Zustand store for persisting custom bid distribution presets to localStorage + +import { SavedCustomPreset } from 'components/Toucan/Auction/BidDistributionChart/dev/customPresets' +import { create } from 'zustand' +import { PersistStorage, persist, StorageValue } from 'zustand/middleware' + +interface CustomPresetsState { + presets: SavedCustomPreset[] + savePreset: (preset: Omit) => void + updatePreset: (id: string, preset: Omit) => void + deletePreset: (id: string) => void + clearAllPresets: () => void +} + +// Custom storage implementation that handles Map serialization +const customStorage: PersistStorage = { + getItem: (name) => { + const str = localStorage.getItem(name) + if (!str) { + return null + } + const parsed = JSON.parse(str) + if (parsed.state?.presets) { + parsed.state.presets = parsed.state.presets + .map((preset: any) => { + let distributionData: Map + + if (preset.distributionData instanceof Map) { + distributionData = preset.distributionData + } else if (Array.isArray(preset.distributionData)) { + distributionData = new Map(preset.distributionData as [string, string][]) + } else if (preset.distributionData && typeof preset.distributionData === 'object') { + distributionData = new Map(Object.entries(preset.distributionData) as [string, string][]) + } else { + distributionData = new Map() + } + + return { ...preset, distributionData } + }) + .filter((preset: SavedCustomPreset) => preset.distributionData.size > 0) + } + return parsed as StorageValue + }, + setItem: (name, value) => { + const serializable = { + ...value, + state: { + ...value.state, + presets: value.state.presets.map((preset: any) => { + // Convert Map to array for JSON serialization + let distributionData: any + if (preset.distributionData instanceof Map) { + distributionData = Array.from(preset.distributionData.entries()) + } else if ( + preset.distributionData && + typeof preset.distributionData === 'object' && + 'entries' in preset.distributionData && + typeof preset.distributionData.entries === 'function' + ) { + // Map-like object with entries method + distributionData = Array.from(preset.distributionData.entries()) + } else if (Array.isArray(preset.distributionData)) { + distributionData = preset.distributionData + } else if (preset.distributionData && typeof preset.distributionData === 'object') { + // Plain object - convert to array format + distributionData = Object.entries(preset.distributionData) + } else { + distributionData = [] + } + + return { + ...preset, + distributionData, + } + }), + }, + } + localStorage.setItem(name, JSON.stringify(serializable)) + }, + removeItem: (name) => { + localStorage.removeItem(name) + }, +} + +export const useCustomPresetsStore = create()( + persist( + (set) => ({ + presets: [], + + savePreset: (preset) => { + const newPreset: SavedCustomPreset = { + ...preset, + id: crypto.randomUUID(), + createdAt: Date.now(), + } + set((state) => ({ + presets: [...state.presets, newPreset], + })) + }, + + updatePreset: (id, preset) => { + set((state) => ({ + presets: state.presets.map((p) => + p.id === id + ? { + ...preset, + id: p.id, // Keep the same ID + createdAt: p.createdAt, // Keep the original creation date + } + : p, + ), + })) + }, + + deletePreset: (id) => { + set((state) => ({ + presets: state.presets.filter((p) => p.id !== id), + })) + }, + + clearAllPresets: () => { + set({ presets: [] }) + }, + }), + { + name: 'toucan-custom-bid-presets', + storage: customStorage, + }, + ), +) diff --git a/apps/web/src/components/Toucan/Auction/BidDistributionChart/hooks/useChartDimensions.ts b/apps/web/src/components/Toucan/Auction/BidDistributionChart/hooks/useChartDimensions.ts new file mode 100644 index 00000000000..31d3e4b0f04 --- /dev/null +++ b/apps/web/src/components/Toucan/Auction/BidDistributionChart/hooks/useChartDimensions.ts @@ -0,0 +1,27 @@ +import type { IChartApi } from 'lightweight-charts' +import { useCallback } from 'react' + +/** + * Hook for calculating distribution chart dimensions and offsets + */ +export function useChartDimensions() { + /** + * Calculate the plot area dimensions (where bars are actually rendered) + * Returns left offset and width of the clipped plot area + */ + const getPlotDimensions = useCallback( + (containerRef: HTMLDivElement | null, chart: IChartApi | null): { left: number; width: number } => { + if (!containerRef || !chart) { + return { left: 0, width: 0 } + } + + const left = chart.priceScale('left').width() + const width = chart.paneSize().width + + return { left: Math.round(left), width: Math.round(width) } + }, + [], + ) + + return { getPlotDimensions } +} diff --git a/apps/web/src/components/Toucan/Auction/BidDistributionChart/hooks/useChartLabels.ts b/apps/web/src/components/Toucan/Auction/BidDistributionChart/hooks/useChartLabels.ts new file mode 100644 index 00000000000..52c21c6d6bd --- /dev/null +++ b/apps/web/src/components/Toucan/Auction/BidDistributionChart/hooks/useChartLabels.ts @@ -0,0 +1,127 @@ +import { COORDINATE_SCALING, LABEL_CONFIG } from 'components/Toucan/Auction/BidDistributionChart/constants' +import { + calculateDynamicLabelIncrement, + formatTickForDisplay, +} from 'components/Toucan/Auction/BidDistributionChart/utils/utils' +import { BidTokenInfo, DisplayMode } from 'components/Toucan/Auction/store/types' +import { IChartApi, UTCTimestamp } from 'lightweight-charts' +import { useCallback } from 'react' +import { UseSporeColorsReturn } from 'ui/src/hooks/useSporeColors' + +interface UseChartLabelsParams { + minTick: number + maxTick: number + labelIncrement: number | undefined + tickSize: number + displayMode: DisplayMode + bidTokenInfo: BidTokenInfo + totalSupply?: string + auctionTokenDecimals: number + formatter: (amount: number) => string + colors: UseSporeColorsReturn +} + +/** + * Hook for managing custom x-axis labels on the chart + */ +export function useChartLabels(params: UseChartLabelsParams) { + const { tickSize, displayMode, bidTokenInfo, totalSupply, auctionTokenDecimals, formatter, colors } = params + + /** + * Creates a single label DOM element with proper styling and positioning + */ + const createLabelElement = useCallback( + (params: { tickValue: number; x: number; plotLeft: number }): HTMLDivElement => { + const { tickValue, x, plotLeft } = params + const label = document.createElement('div') + Object.assign(label.style, { + position: 'absolute', + left: `${x + plotLeft}px`, + bottom: '0', + transform: 'translateX(-50%)', // Center label on the x coordinate + color: colors.neutral2.val, + fontSize: `${LABEL_CONFIG.FONT_SIZE}px`, + lineHeight: `${LABEL_CONFIG.LINE_HEIGHT}px`, + whiteSpace: 'nowrap', + }) + + // Format label based on display mode + const formattedValue = formatTickForDisplay({ + tickValue, + displayMode, + bidTokenInfo, + totalSupply, + auctionTokenDecimals, + formatter, + }) + label.textContent = formattedValue + + return label + }, + [displayMode, bidTokenInfo, totalSupply, auctionTokenDecimals, formatter, colors.neutral2.val], + ) + + /** + * Renders custom x-axis labels at evenly spaced intervals + * Dynamically calculates label increment based on the visible range + */ + const renderLabels = useCallback( + (params: { labelsLayer: HTMLDivElement; chart: IChartApi; plotLeft: number }) => { + const { labelsLayer, chart, plotLeft } = params + labelsLayer.innerHTML = '' + + // Get the current visible range from the chart + // Wrap in try-catch because lightweight-charts throws an error instead of returning null + // when the chart is not fully initialized (e.g., on initial load or display mode change) + let visibleRange + try { + visibleRange = chart.timeScale().getVisibleRange() + } catch { + // Chart not initialized yet, skip rendering labels + return + } + + // Guard against null - chart may not be initialized yet + if (!visibleRange) { + return + } + + // Convert visible range from scaled coordinates to tick values + const visibleFromTick = (visibleRange.from as number) / COORDINATE_SCALING.PRICE_SCALE_FACTOR + const visibleToTick = (visibleRange.to as number) / COORDINATE_SCALING.PRICE_SCALE_FACTOR + + // Calculate dynamic label increment based on currently visible range + const dynamicLabelIncrement = calculateDynamicLabelIncrement({ + visibleFrom: visibleFromTick, + visibleTo: visibleToTick, + tickSize, + }) + + // Find the first label index (multiple of dynamicLabelIncrement that's >= visibleFromTick) + const startIndex = Math.ceil(visibleFromTick / dynamicLabelIncrement) + const endIndex = Math.floor(visibleToTick / dynamicLabelIncrement) + + // Generate labels using multiplier index to avoid floating-point drift + for (let multiplierIndex = startIndex; multiplierIndex <= endIndex; multiplierIndex++) { + // Calculate tick value directly from multiplier (avoids floating-point errors) + const tickValue = multiplierIndex * dynamicLabelIncrement + + // Convert tick value to time coordinate (must match bar data points) + const timeValue = Math.round(tickValue * COORDINATE_SCALING.PRICE_SCALE_FACTOR) as UTCTimestamp + const x = chart.timeScale().timeToCoordinate(timeValue) + + // Skip if calculated label position doesn't correspond to an actual data point + // (timeToCoordinate returns null when the time value doesn't exist in the chart's data) + if (x == null) { + continue + } + + const label = createLabelElement({ tickValue, x, plotLeft }) + labelsLayer.appendChild(label) + } + }, + [tickSize, createLabelElement], + ) + + return { renderLabels } +} diff --git a/apps/web/src/components/Toucan/Auction/BidDistributionChart/hooks/useChartTooltip.ts b/apps/web/src/components/Toucan/Auction/BidDistributionChart/hooks/useChartTooltip.ts new file mode 100644 index 00000000000..7a81d28f279 --- /dev/null +++ b/apps/web/src/components/Toucan/Auction/BidDistributionChart/hooks/useChartTooltip.ts @@ -0,0 +1,72 @@ +import { TOOLTIP_CONFIG } from 'components/Toucan/Auction/BidDistributionChart/constants' +import { formatTickForDisplay } from 'components/Toucan/Auction/BidDistributionChart/utils/utils' +import { BidTokenInfo, DisplayMode } from 'components/Toucan/Auction/store/types' +import { useCallback } from 'react' +import { useTranslation } from 'react-i18next' +import { UseSporeColorsReturn } from 'ui/src/hooks/useSporeColors' +import { zIndexes } from 'ui/src/theme' + +interface UseChartTooltipParams { + displayMode: DisplayMode + bidTokenInfo: BidTokenInfo + totalSupply?: string + auctionTokenDecimals: number + formatter: (amount: number) => string + volumeFormatter: (amount: number) => string + colors: UseSporeColorsReturn +} + +/** + * Manages tooltip style + text that shows when user hovers over chart bar + */ +export function useChartTooltip(params: UseChartTooltipParams) { + const { displayMode, bidTokenInfo, totalSupply, auctionTokenDecimals, formatter, volumeFormatter, colors } = params + const { t } = useTranslation() + + const fdvText = t('stats.fdv') + + const createTooltipElement = useCallback((): HTMLDivElement => { + const tooltip = document.createElement('div') + Object.assign(tooltip.style, { + position: 'absolute', + pointerEvents: 'none', + background: colors.surface2.val, + color: colors.neutral1.val, + zIndex: String(zIndexes.tooltip), + fontSize: `${TOOLTIP_CONFIG.FONT_SIZE}px`, + padding: TOOLTIP_CONFIG.PADDING, + borderRadius: TOOLTIP_CONFIG.BORDER_RADIUS, + transform: `translate(-50%, -${TOOLTIP_CONFIG.VERTICAL_OFFSET_PERCENT}%)`, + whiteSpace: 'nowrap', + display: 'none', + }) + return tooltip + }, [colors.neutral1.val, colors.surface2.val]) + + /** + * Formats tooltip text based on display mode, tick value, and volume amount + */ + const formatTooltipText = useCallback( + (tickValue: number, volumeAmount: number): string => { + const tickDisplay = formatTickForDisplay({ + tickValue, + displayMode, + bidTokenInfo, + totalSupply, + auctionTokenDecimals, + formatter, + }) + + // Handle zero values explicitly to show "0" instead of "-" + const volumeDisplay = volumeAmount === 0 ? '0' : volumeFormatter(volumeAmount) + + // Add "FDV" suffix when in valuation mode + const suffix = displayMode === DisplayMode.VALUATION ? ` ${fdvText}` : '' + + return `${volumeDisplay} @ ${tickDisplay}${suffix}` + }, + [displayMode, bidTokenInfo, totalSupply, auctionTokenDecimals, formatter, volumeFormatter, fdvText], + ) + + return { createTooltipElement, formatTooltipText } +} diff --git a/apps/web/src/components/Toucan/Auction/BidDistributionChart/utils/bidConcentration.ts b/apps/web/src/components/Toucan/Auction/BidDistributionChart/utils/bidConcentration.ts new file mode 100644 index 00000000000..bc8542d20f6 --- /dev/null +++ b/apps/web/src/components/Toucan/Auction/BidDistributionChart/utils/bidConcentration.ts @@ -0,0 +1,102 @@ +/** + * Utility functions for calculating bid concentration bands + */ + +export interface BidConcentrationResult { + startTick: number // Starting tick value + endTick: number // Ending tick value + startIndex: number // Starting bar index in original bars array + endIndex: number // Ending bar index in original bars array (inclusive) + percentage: number // Percentage of total volume (0-1) +} + +interface BarWithVolume { + tick: number + amount: number + index: number +} + +interface BidConcentrationOptions { + bars: BarWithVolume[] + clearingPrice: number + targetPercentage?: number + minClusterSize?: number +} + +/** + * Calculate bid concentration: find the largest sequential cluster of bars + * that contains approximately 80% of the total bid volume. + * Only considers bids at or above the clearing price. + * + * @param options - Configuration options + * @param options.bars - Array of bars with tick, amount, and index + * @param options.clearingPrice - Clearing price (only bars >= this price are considered) + * @param options.targetPercentage - Target percentage of total volume (default 0.8 for 80%) + * @param options.minClusterSize - Minimum number of bars required to show concentration (default 3) + * @returns Concentration band info or null if no suitable cluster found + */ +export function calculateBidConcentration({ + bars, + clearingPrice, + targetPercentage = 0.8, + minClusterSize = 3, +}: BidConcentrationOptions): BidConcentrationResult | null { + // Filter out bars with zero amount AND bars below clearing price + // IMPORTANT: Keep original indices intact for renderer to use + const eligibleBars = bars.filter((bar) => { + if (bar.amount === 0) { + return false + } + // Only include bars at or above clearing price + if (bar.tick < clearingPrice) { + return false + } + return true + }) + + if (eligibleBars.length < minClusterSize) { + return null + } + + // Calculate total volume (sum of all bid amounts in fiat) + const totalVolume = eligibleBars.reduce((sum, bar) => sum + bar.amount, 0) + + if (totalVolume === 0) { + return null + } + + const targetVolume = totalVolume * targetPercentage + + // Try to find the smallest sequential window that captures >= targetVolume + let bestCluster: BidConcentrationResult | null = null + let bestClusterSize = Infinity + + // Sliding window approach: for each starting position, expand window until we reach target + for (let start = 0; start < eligibleBars.length; start++) { + let windowVolume = 0 + for (let end = start; end < eligibleBars.length; end++) { + windowVolume += eligibleBars[end].amount + + // If we've reached or exceeded the target volume + if (windowVolume >= targetVolume) { + const clusterSize = end - start + 1 + + // Track the smallest cluster that meets the target + if (clusterSize < bestClusterSize && clusterSize >= minClusterSize) { + bestClusterSize = clusterSize + bestCluster = { + startTick: eligibleBars[start].tick, + endTick: eligibleBars[end].tick, + // Use original bar.index from the full bars array, not the filtered array index + startIndex: eligibleBars[start].index, + endIndex: eligibleBars[end].index, + percentage: windowVolume / totalVolume, + } + } + break // Move to next starting position + } + } + } + + return bestCluster +} diff --git a/apps/web/src/components/Toucan/Auction/BidDistributionChart/utils/clearingPrice/label.ts b/apps/web/src/components/Toucan/Auction/BidDistributionChart/utils/clearingPrice/label.ts new file mode 100644 index 00000000000..38b1b345eae --- /dev/null +++ b/apps/web/src/components/Toucan/Auction/BidDistributionChart/utils/clearingPrice/label.ts @@ -0,0 +1,61 @@ +import { formatTickForDisplay } from 'components/Toucan/Auction/BidDistributionChart/utils/utils' +import { BidTokenInfo, DisplayMode } from 'components/Toucan/Auction/store/types' + +/** + * Parameters for formatting a clearing price label + */ +interface FormatClearingPriceLabelParams { + clearingPrice: number // Clearing price in decimal form + displayMode: DisplayMode // Current display mode (token price vs valuation) + bidTokenInfo: BidTokenInfo // Token information for formatting + totalSupply?: string // Total supply for valuation calculations + auctionTokenDecimals: number // Decimals for the auction token + formatter: (amount: number) => string // Number formatter function +} + +/** + * Formats a clearing price value for display on the chart label. + * + * This pure function creates a formatted label string that's consistent with + * the chart's tick labels and display mode. It's decoupled from the tooltip + * formatting logic, making it independently testable and reusable. + * + * @param params - Formatting parameters + * @returns Formatted clearing price string + * + * @example + * ```typescript + * // Token price mode + * formatClearingPriceLabel({ + * clearingPrice: 1.5, + * displayMode: DisplayMode.TOKEN_PRICE, + * bidTokenInfo: { decimals: 6, ... }, + * formatter: (n) => `$${n.toFixed(2)}` + * }) + * // Returns: "$1.50" + * + * // Valuation mode with FDV suffix + * formatClearingPriceLabel({ + * clearingPrice: 1500000, + * displayMode: DisplayMode.VALUATION, + * totalSupply: "1000000", + * ... + * }) + * // Returns: "$1.5M FDV" + * ``` + */ +export function formatClearingPriceLabel(params: FormatClearingPriceLabelParams): string { + const { clearingPrice, displayMode, bidTokenInfo, totalSupply, auctionTokenDecimals, formatter } = params + + // Use the same formatter as chart tick labels for consistency + const formattedValue = formatTickForDisplay({ + tickValue: clearingPrice, + displayMode, + bidTokenInfo, + totalSupply, + auctionTokenDecimals, + formatter, + }) + + return formattedValue +} diff --git a/apps/web/src/components/Toucan/Auction/BidDistributionChart/utils/clearingPrice/position.test.ts b/apps/web/src/components/Toucan/Auction/BidDistributionChart/utils/clearingPrice/position.test.ts new file mode 100644 index 00000000000..3e00f7eebd6 --- /dev/null +++ b/apps/web/src/components/Toucan/Auction/BidDistributionChart/utils/clearingPrice/position.test.ts @@ -0,0 +1,292 @@ +import { COORDINATE_SCALING } from 'components/Toucan/Auction/BidDistributionChart/constants' +import { findClearingPriceXPosition } from 'components/Toucan/Auction/BidDistributionChart/utils/clearingPrice/position' + +describe('findClearingPriceXPosition', () => { + describe('exact position match', () => { + it('returns bar center when clearing price exactly matches bar tick value', () => { + const bars = [ + { tickValue: 1.0, column: { left: 10, right: 20 } }, + { tickValue: 2.0, column: { left: 30, right: 40 } }, + ] + + const result = findClearingPriceXPosition({ + clearingPrice: 1.0, + bars, + }) + + expect(result).toBe(15) // (10 + 20) / 2 + }) + + it('returns bar center when clearing price is within tolerance of bar', () => { + const bars = [{ tickValue: 1.0, column: { left: 10, right: 20 } }] + + // Clearing price very close to 1.0 (within default tolerance of 1 scaled unit) + const scaleFactor = COORDINATE_SCALING.PRICE_SCALE_FACTOR + const almostOne = (scaleFactor - 0.5) / scaleFactor + + const result = findClearingPriceXPosition({ + clearingPrice: almostOne, + bars, + }) + + expect(result).toBe(15) // Should match despite slight difference + }) + + it('uses custom position tolerance when provided', () => { + const bars = [{ tickValue: 1.0, column: { left: 10, right: 20 } }] + + // With larger tolerance, should match + const result1 = findClearingPriceXPosition({ + clearingPrice: 1.01, + bars, + positionTolerance: 150, // Increased tolerance + }) + expect(result1).toBe(15) + + // With stricter tolerance, should not match + const result2 = findClearingPriceXPosition({ + clearingPrice: 1.01, + bars, + positionTolerance: 0.1, // Very strict tolerance + }) + expect(result2).toBeNull() + }) + }) + + describe('interpolation between bars', () => { + it('interpolates position when clearing price falls between two bars', () => { + const bars = [ + { tickValue: 1.0, column: { left: 10, right: 20 } }, // center: 15 + { tickValue: 2.0, column: { left: 30, right: 40 } }, // center: 35 + ] + + // Clearing price exactly halfway between bars + const result = findClearingPriceXPosition({ + clearingPrice: 1.5, + bars, + }) + + expect(result).toBe(25) // Halfway between 15 and 35 + }) + + it('interpolates correctly at 25% ratio', () => { + const bars = [ + { tickValue: 1.0, column: { left: 0, right: 10 } }, // center: 5 + { tickValue: 2.0, column: { left: 20, right: 30 } }, // center: 25 + ] + + // Clearing price 25% of the way from 1.0 to 2.0 + const result = findClearingPriceXPosition({ + clearingPrice: 1.25, + bars, + }) + + expect(result).toBe(10) // 5 + 0.25 * (25 - 5) = 10 + }) + + it('interpolates correctly at 75% ratio', () => { + const bars = [ + { tickValue: 1.0, column: { left: 0, right: 10 } }, // center: 5 + { tickValue: 2.0, column: { left: 20, right: 30 } }, // center: 25 + ] + + // Clearing price 75% of the way from 1.0 to 2.0 + const result = findClearingPriceXPosition({ + clearingPrice: 1.75, + bars, + }) + + expect(result).toBe(20) // 5 + 0.75 * (25 - 5) = 20 + }) + + it('handles non-uniform bar spacing', () => { + const bars = [ + { tickValue: 1.0, column: { left: 0, right: 5 } }, // center: 2.5 + { tickValue: 3.0, column: { left: 100, right: 120 } }, // center: 110 + ] + + // Clearing price exactly halfway (2.0) between 1.0 and 3.0 + const result = findClearingPriceXPosition({ + clearingPrice: 2.0, + bars, + }) + + expect(result).toBe(56.25) // 2.5 + 0.5 * (110 - 2.5) = 56.25 + }) + }) + + describe('edge cases', () => { + it('returns null when bars array is empty', () => { + const result = findClearingPriceXPosition({ + clearingPrice: 1.5, + bars: [], + }) + + expect(result).toBeNull() + }) + + it('returns null when no bars have column data', () => { + const bars = [{ tickValue: 1.0 }, { tickValue: 2.0 }] + + const result = findClearingPriceXPosition({ + clearingPrice: 1.5, + bars, + }) + + expect(result).toBeNull() + }) + + it('returns null when clearing price is below all bars', () => { + const bars = [ + { tickValue: 2.0, column: { left: 20, right: 30 } }, + { tickValue: 3.0, column: { left: 40, right: 50 } }, + ] + + const result = findClearingPriceXPosition({ + clearingPrice: 1.0, // Below all bars + bars, + }) + + expect(result).toBeNull() + }) + + it('returns null when clearing price is above all bars', () => { + const bars = [ + { tickValue: 1.0, column: { left: 10, right: 20 } }, + { tickValue: 2.0, column: { left: 30, right: 40 } }, + ] + + const result = findClearingPriceXPosition({ + clearingPrice: 3.0, // Above all bars + bars, + }) + + expect(result).toBeNull() + }) + + it('skips bars without column data and continues searching', () => { + const bars = [ + { tickValue: 1.0 }, // No column + { tickValue: 1.5, column: { left: 10, right: 20 } }, + { tickValue: 2.0, column: { left: 30, right: 40 } }, + ] + + const result = findClearingPriceXPosition({ + clearingPrice: 1.75, + bars, + }) + + // Should interpolate between second and third bars (skipping first) + expect(result).toBe(25) // Halfway between 15 and 35 + }) + + it('handles single bar with exact match', () => { + const bars = [{ tickValue: 1.5, column: { left: 10, right: 20 } }] + + const result = findClearingPriceXPosition({ + clearingPrice: 1.5, + bars, + }) + + expect(result).toBe(15) + }) + + it('handles single bar without match', () => { + const bars = [{ tickValue: 1.0, column: { left: 10, right: 20 } }] + + const result = findClearingPriceXPosition({ + clearingPrice: 2.0, + bars, + }) + + expect(result).toBeNull() + }) + }) + + describe('precision and rounding', () => { + it('handles decimal tick values correctly', () => { + const bars = [ + { tickValue: 1.123, column: { left: 10, right: 20 } }, + { tickValue: 1.456, column: { left: 30, right: 40 } }, + ] + + const result = findClearingPriceXPosition({ + clearingPrice: 1.2895, // Midpoint + bars, + }) + + expect(result).toBeCloseTo(25, 1) // Should be approximately halfway + }) + + it('handles very small tick differences', () => { + const bars = [ + { tickValue: 0.0001, column: { left: 10, right: 20 } }, + { tickValue: 0.0002, column: { left: 30, right: 40 } }, + ] + + const result = findClearingPriceXPosition({ + clearingPrice: 0.00015, + bars, + }) + + // Note: Scale factor (10000) causes rounding. 0.00015 * 10000 = 1.5 → rounds to 2 + // This matches bar1 (0.0001 * 10000 = 1), so returns bar1 center + expect(result).toBe(15) + }) + + it('handles large tick values', () => { + const bars = [ + { tickValue: 1000000, column: { left: 10, right: 20 } }, + { tickValue: 2000000, column: { left: 30, right: 40 } }, + ] + + const result = findClearingPriceXPosition({ + clearingPrice: 1500000, + bars, + }) + + expect(result).toBe(25) // Halfway + }) + }) + + describe('real-world scenarios', () => { + it('handles typical auction bid distribution with multiple bars', () => { + // Simulating a real auction with 5 price points + const bars = [ + { tickValue: 1.0, column: { left: 0, right: 10 } }, + { tickValue: 1.25, column: { left: 15, right: 25 } }, + { tickValue: 1.5, column: { left: 30, right: 40 } }, // Clearing price near here + { tickValue: 1.75, column: { left: 45, right: 55 } }, + { tickValue: 2.0, column: { left: 60, right: 70 } }, + ] + + // Clearing price slightly above 1.5 + const result = findClearingPriceXPosition({ + clearingPrice: 1.6, + bars, + }) + + // Should interpolate between bars at 1.5 and 1.75 + // Bar centers: 35 and 50 + // Ratio: (1.6 - 1.5) / (1.75 - 1.5) = 0.1 / 0.25 = 0.4 + // Result: 35 + 0.4 * (50 - 35) = 41 + expect(result).toBe(41) + }) + + it('handles bars with varying widths', () => { + const bars = [ + { tickValue: 1.0, column: { left: 0, right: 5 } }, // Narrow bar + { tickValue: 2.0, column: { left: 10, right: 30 } }, // Wide bar + ] + + const result = findClearingPriceXPosition({ + clearingPrice: 1.5, + bars, + }) + + // Centers: 2.5 and 20 + // Halfway: 2.5 + 0.5 * (20 - 2.5) = 11.25 + expect(result).toBe(11.25) + }) + }) +}) diff --git a/apps/web/src/components/Toucan/Auction/BidDistributionChart/utils/clearingPrice/position.ts b/apps/web/src/components/Toucan/Auction/BidDistributionChart/utils/clearingPrice/position.ts new file mode 100644 index 00000000000..8896fd37602 --- /dev/null +++ b/apps/web/src/components/Toucan/Auction/BidDistributionChart/utils/clearingPrice/position.ts @@ -0,0 +1,143 @@ +import { COORDINATE_SCALING } from 'components/Toucan/Auction/BidDistributionChart/constants' + +/** + * Represents a bar's position and tick value for clearing price calculations + */ +interface BarPosition { + tickValue: number + column?: { + left: number + right: number + } +} + +/** + * Parameters for finding the clearing price X position + */ +interface FindClearingPriceXPositionParams { + clearingPrice: number // Clearing price in decimal form + bars: BarPosition[] // Array of bars with position and tick information + positionTolerance?: number // Tolerance for exact position matching (default: 1) +} + +/** + * Finds the X-coordinate for drawing a vertical line at the clearing price position. + * + * This pure function handles three cases: + * 1. Clearing price exactly matches a bar's position → returns bar center + * 2. Clearing price falls between two bars → interpolates between their centers + * 3. Clearing price outside bar range → returns null + * + * @param params - Parameters for position calculation + * @returns X-coordinate in pixels, or null if position cannot be determined + * + * @example + * ```typescript + * const lineX = findClearingPriceXPosition({ + * clearingPrice: 1.5, + * bars: [ + * { tickValue: 1.0, column: { left: 10, right: 20 } }, + * { tickValue: 2.0, column: { left: 30, right: 40 } } + * ] + * }) + * // Returns: 25 (interpolated between bars) + * ``` + */ +export function findClearingPriceXPosition(params: FindClearingPriceXPositionParams): number | null { + const { clearingPrice, bars, positionTolerance = 1 } = params + + // Convert clearing price to scaled coordinate for comparison + const clearingPriceScaled = Math.round(clearingPrice * COORDINATE_SCALING.PRICE_SCALE_FACTOR) + + // Find bars surrounding the clearing price + for (let i = 0; i < bars.length; i++) { + const bar = bars[i] + const nextBar = bars[i + 1] + + // Skip bars without column data + if (!bar.column) { + continue + } + + const barTimeScaled = Math.round(bar.tickValue * COORDINATE_SCALING.PRICE_SCALE_FACTOR) + + // Case 1: Clearing price exactly at this bar + if (Math.abs(barTimeScaled - clearingPriceScaled) < positionTolerance) { + return getBarCenter(bar.column) + } + + // Case 2: Clearing price between this bar and next + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!nextBar || !nextBar.column) { + continue + } + + const nextBarTimeScaled = Math.round(nextBar.tickValue * COORDINATE_SCALING.PRICE_SCALE_FACTOR) + + if (barTimeScaled <= clearingPriceScaled && clearingPriceScaled <= nextBarTimeScaled) { + return interpolateBarPosition({ + clearingPriceScaled, + bar1: { tickValue: barTimeScaled, column: bar.column }, + bar2: { tickValue: nextBarTimeScaled, column: nextBar.column }, + }) + } + } + + // Case 3: Position not found + return null +} + +/** + * Calculates the center X coordinate of a bar column + */ +function getBarCenter(column: { left: number; right: number }): number { + return (column.left + column.right) / 2 +} + +/** + * Parameters for interpolating between two bar positions + */ +interface InterpolateBarPositionParams { + clearingPriceScaled: number + bar1: { tickValue: number; column: { left: number; right: number } } + bar2: { tickValue: number; column: { left: number; right: number } } +} + +/** + * Interpolates the X position between two bars based on clearing price ratio. + * + * Uses linear interpolation to find the exact position where the clearing price + * line should be drawn when it falls between two bar positions. + * + * @param params - Interpolation parameters + * @returns Interpolated X-coordinate in pixels + * + * @example + * ```typescript + * // Clearing price at 1.5, bar1 at 1.0, bar2 at 2.0 + * // Returns position 50% between the two bars + * interpolateBarPosition({ + * clearingPriceScaled: 15000, + * bar1: { tickValue: 10000, column: { left: 10, right: 20 } }, + * bar2: { tickValue: 20000, column: { left: 30, right: 40 } } + * }) + * ``` + */ +function interpolateBarPosition(params: InterpolateBarPositionParams): number { + const { clearingPriceScaled, bar1, bar2 } = params + + // Guard against division by zero when bars have identical tick values + if (bar2.tickValue === bar1.tickValue) { + return getBarCenter(bar1.column) + } + + // Calculate ratio: how far between bar1 and bar2 is the clearing price? + const ratio = (clearingPriceScaled - bar1.tickValue) / (bar2.tickValue - bar1.tickValue) + + // Get center positions of both bars + const bar1CenterX = getBarCenter(bar1.column) + const bar2CenterX = getBarCenter(bar2.column) + + // Linear interpolation between the two centers + return bar1CenterX + ratio * (bar2CenterX - bar1CenterX) +} diff --git a/apps/web/src/components/Toucan/Auction/BidDistributionChart/utils/utils.ts b/apps/web/src/components/Toucan/Auction/BidDistributionChart/utils/utils.ts new file mode 100644 index 00000000000..69619796f21 --- /dev/null +++ b/apps/web/src/components/Toucan/Auction/BidDistributionChart/utils/utils.ts @@ -0,0 +1,494 @@ +import { + CHART_CONSTRAINTS, + DEFAULT_Y_AXIS_LEVELS, + LABEL_GENERATION, + NICE_VALUES, + TOLERANCE, +} from 'components/Toucan/Auction/BidDistributionChart/constants' +import { + BidConcentrationResult, + calculateBidConcentration, +} from 'components/Toucan/Auction/BidDistributionChart/utils/bidConcentration' +import { BidDistributionData, BidTokenInfo, DisplayMode } from 'components/Toucan/Auction/store/types' +import { formatUnits } from 'viem' + +/** + * Represents a single bar in the distribution chart + */ +// eslint-disable-next-line import/no-unused-modules +export interface ChartBarData { + tick: number // Tick value in smallest unit + tickDisplay: string // Formatted tick for display + amount: number // Bid amount in USD (converted from base token using bidTokenInfo.priceFiat) + index: number // Bar index for positioning +} + +/** + * Processed chart data with calculated axis information + */ +// eslint-disable-next-line import/no-unused-modules +export interface ProcessedChartData { + bars: ChartBarData[] + yAxisLevels: number[] + minTick: number + maxTick: number + maxAmount: number + labelIncrement?: number + concentration: BidConcentrationResult | null +} + +/** + * Converts a value from smallest unit to decimal using viem's formatUnits + * + * @param value - Value in smallest unit (as string) + * @param decimals - Number of decimals + * @returns Decimal number + */ +function toDecimal(value: string, decimals: number): number { + return Number(formatUnits(BigInt(value), decimals)) +} + +/** + * Calculate bar step size and range for the chart + * Rules: + * 1. Minimum 20 bars total + * 2. Bars step by tick_size multiples + * 3. Range extends beyond data if needed to reach 20 bars + */ +function calculateBarStepAndRange(params: { minTick: number; maxTick: number; tickSize: number }): { + barStep: number // Step size between bars (multiple of tick_size) + rangeMax: number // Adjusted max tick to achieve min 20 bars + totalBars: number // Total number of bars +} { + const { minTick, maxTick, tickSize } = params + + // Calculate how many tick_size steps exist in the data range + const dataSteps = Math.round((maxTick - minTick) / tickSize) + 1 + + if (dataSteps >= CHART_CONSTRAINTS.MIN_BARS) { + // We have enough steps, use them all + return { + barStep: tickSize, + rangeMax: maxTick, + totalBars: dataSteps, + } + } else { + // Need to extend range to reach MIN_BARS + // Calculate how many tick_size steps needed beyond maxTick + const additionalSteps = CHART_CONSTRAINTS.MIN_BARS - dataSteps + const rangeMax = maxTick + additionalSteps * tickSize + + return { + barStep: tickSize, + rangeMax, + totalBars: CHART_CONSTRAINTS.MIN_BARS, + } + } +} + +/** + * Normalizes a value to its order of magnitude and normalized form + * @param value - The value to normalize + * @returns Object with magnitude (power of 10) and normalized value (1-10 range) + */ +function normalizeValue(value: number): { magnitude: number; normalized: number } { + if (value <= 0) { + return { magnitude: 1, normalized: 1 } + } + const magnitude = Math.pow(10, Math.floor(Math.log10(value))) + const normalized = value / magnitude + return { magnitude, normalized } +} + +/** + * Rounds a number to the nearest "nice" value for chart labels + * Nice values are typically 1, 2, 5 (or with 2.5 for finer granularity) + * + * @example + * roundToNiceNumber(350) // 500 (normalized: 3.5 rounds up to 5, magnitude: 100) + * roundToNiceNumber(350, [1, 2, 2.5, 5]) // 500 + * roundToNiceNumber(230, [1, 2, 2.5, 5]) // 250 (2.5 * 100) + * + * @param value - The value to round + * @param niceValues - Array of normalized "nice" values (default: NICE_VALUES.STANDARD) + * @returns The rounded "nice" number + */ +function roundToNiceNumber(value: number, niceValues: readonly number[] = NICE_VALUES.STANDARD): number { + const { magnitude, normalized } = normalizeValue(value) + + // Find the first nice value >= normalized + for (const nice of niceValues) { + if (normalized <= nice) { + return nice * magnitude + } + } + + // If normalized exceeds all nice values, scale up to next magnitude + return niceValues[0] * magnitude * 10 +} + +/** + * Generates candidate multipliers dynamically based on the range + * Creates "nice" round numbers that produce the target number of labels + */ +function generateDynamicCandidates(params: { minMultiplier: number; maxMultiplier: number }): number[] { + const { minMultiplier, maxMultiplier } = params + + const candidates = new Set() + + // Always include small multipliers for small ranges + const baseMultipliers = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 25, 30, 40, 50, 60, 75, 80, 100] + for (const m of baseMultipliers) { + if (m >= minMultiplier && m <= maxMultiplier) { + candidates.add(m) + } + } + + // Generate nice round numbers within the range + // Use powers of 10, 5, and 2 to create nice increments + let current = Math.max(1, minMultiplier) + while (current <= maxMultiplier) { + const rounded = roundToNiceNumber(current) + if (rounded >= minMultiplier && rounded <= maxMultiplier) { + candidates.add(rounded) + } + + // Step by nice increments + if (current < 10) { + current += 1 + } else if (current < 100) { + current += 10 + } else if (current < 1000) { + current += 50 + } else { + current += 100 + } + } + + // Always include the boundary values rounded to nice numbers + candidates.add(roundToNiceNumber(minMultiplier)) + candidates.add(roundToNiceNumber(maxMultiplier)) + + return Array.from(candidates).sort((a, b) => a - b) +} + +/** + * Checks if a multiplier is a "nice" round number + * Nice numbers are values like 1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, etc. + * + * @example + * isNiceMultiplier(50) // true (5 * 10) + * isNiceMultiplier(75) // false (7.5 is not in [1, 2, 5]) + * + * @param multiplier - The multiplier to check + * @param niceValues - Array of normalized "nice" values (default: NICE_VALUES.STANDARD) + * @returns true if the multiplier is a nice round number + */ +function isNiceMultiplier(multiplier: number, niceValues: readonly number[] = NICE_VALUES.STANDARD): boolean { + const { normalized } = normalizeValue(multiplier) + return niceValues.includes(normalized) +} + +/** + * Calculate x-axis label increment for optimal evenly spaced labels + * Labels align with tick_size boundaries and aim for 7-12 total labels + * Prefers "nice" round numbers (multiples of 1, 2, 5, 10, 20, 50, 100, etc.) + * Dynamically handles any range size from tiny to extreme + */ +function calculateLabelIncrement(params: { minTick: number; rangeMax: number; tickSize: number }): number { + const { minTick, rangeMax, tickSize } = params + const range = rangeMax - minTick + + // Calculate theoretical multiplier bounds to achieve target label count + // minMultiplier produces MAX_LABELS, maxMultiplier produces MIN_LABELS + const minMultiplier = Math.max(1, Math.ceil(range / (LABEL_GENERATION.MAX_LABELS * tickSize))) + const maxMultiplier = Math.max(1, Math.floor(range / (LABEL_GENERATION.MIN_LABELS * tickSize))) + + // Generate candidates dynamically based on the actual range + const candidates = generateDynamicCandidates({ minMultiplier, maxMultiplier }) + + let bestMultiplier = 0 + let bestScore = Infinity + + // Evaluate all candidates + for (const multiplier of candidates) { + const labelIncrement = multiplier * tickSize + const numLabels = Math.floor(range / labelIncrement) + 1 + + // Skip if outside acceptable range + if (numLabels < LABEL_GENERATION.MIN_LABELS || numLabels > LABEL_GENERATION.MAX_LABELS) { + continue + } + + // Score based on: distance from ideal count + preference for nice round numbers + const countDiff = Math.abs(numLabels - LABEL_GENERATION.IDEAL_LABELS) + const roundnessBonus = isNiceMultiplier(multiplier) ? -2 : 0 + const score = countDiff + roundnessBonus + + if (score < bestScore) { + bestScore = score + bestMultiplier = multiplier + } + } + + // Fallback: if no candidate produces 7-12 labels, calculate the ideal multiplier + if (bestMultiplier === 0) { + const idealMultiplier = Math.max(1, Math.ceil(range / (LABEL_GENERATION.IDEAL_LABELS * tickSize))) + bestMultiplier = roundToNiceNumber(idealMultiplier) + + // Validate the fallback produces acceptable label count + const fallbackLabelCount = Math.floor(range / (bestMultiplier * tickSize)) + 1 + + // If still outside range, relax to nearest boundary + if (fallbackLabelCount < LABEL_GENERATION.MIN_LABELS) { + // Too few labels - reduce multiplier to increase label count + bestMultiplier = Math.max(1, Math.floor(range / ((LABEL_GENERATION.MIN_LABELS - 1) * tickSize))) + } else if (fallbackLabelCount > LABEL_GENERATION.MAX_LABELS) { + // Too many labels - increase multiplier to decrease label count + bestMultiplier = Math.max(1, Math.ceil(range / ((LABEL_GENERATION.MAX_LABELS + 1) * tickSize))) + } + } + + return bestMultiplier * tickSize +} + +/** + * Calculate 6 Y-axis levels based on the maximum bid amount + */ +function calculateYAxisLevels(maxAmount: number): number[] { + if (maxAmount === 0) { + return [...DEFAULT_Y_AXIS_LEVELS] + } + + // Calculate a nice increment for 5 steps (6 levels including 0), using Y-axis nice values (including 2.5) + const niceIncrement = roundToNiceNumber(maxAmount / 5, NICE_VALUES.WITH_HALF) + return [0, niceIncrement] +} + +function calculateTickDisplayValue(params: { + tickValue: number + displayMode: DisplayMode + bidTokenInfo: BidTokenInfo + totalSupply?: string + auctionTokenDecimals?: number +}): number { + const { tickValue, displayMode, bidTokenInfo, totalSupply, auctionTokenDecimals = 18 } = params + // Convert tick value to USD (tick is already in bid token units, multiply by price) + const tickInUSD = tickValue * bidTokenInfo.priceFiat + + if (displayMode === DisplayMode.TOKEN_PRICE) { + // Show as token price in USD + return tickInUSD + } else { + // Valuation mode: multiply by total supply to get FDV + if (!totalSupply) { + return tickInUSD + } + + // Convert totalSupply to decimal tokens using the auction token decimals + const totalTokens = toDecimal(totalSupply, auctionTokenDecimals) + return tickInUSD * totalTokens + } +} + +/** + * Format tick value for display (exported for chart component) + * Uses provided formatter function for localized formatting + */ +export function formatTickForDisplay(params: { + tickValue: number + displayMode: DisplayMode + bidTokenInfo: BidTokenInfo + totalSupply?: string + auctionTokenDecimals?: number + formatter: (amount: number) => string +}): string { + const { formatter, ...rest } = params + const displayValue = calculateTickDisplayValue(rest) + return formatter(displayValue) +} + +/** + * Generate chart data from raw bid distribution data + */ +export function generateChartData(params: { + bidData: BidDistributionData + bidTokenInfo: BidTokenInfo + displayMode: DisplayMode + totalSupply?: string + auctionTokenDecimals?: number + clearingPrice: string + tickSize: string + formatter: (amount: number) => string +}): ProcessedChartData { + const { + bidData, + bidTokenInfo, + displayMode, + totalSupply, + auctionTokenDecimals = 18, + clearingPrice, + tickSize, + formatter, + } = params + + // Convert tick_size to decimal + const tickSizeDecimal = toDecimal(tickSize, bidTokenInfo.decimals) + + // Convert map entries to sorted array using BigInt-safe conversion + const entries = Array.from(bidData.entries()) + .map(([tick, amount]) => ({ + tick: toDecimal(tick, bidTokenInfo.decimals), + amount: toDecimal(amount, bidTokenInfo.decimals) * bidTokenInfo.priceFiat, // Convert to USD + })) + .sort((a, b) => a.tick - b.tick) + + if (entries.length === 0) { + // Derive maxTick from tickSize: maxTick = minTick + (MIN_BARS - 1) * tickSize ensures exactly 20 bars + const emptyMaxTick = (CHART_CONSTRAINTS.MIN_BARS - 1) * tickSizeDecimal + + return { + bars: [], + yAxisLevels: [...DEFAULT_Y_AXIS_LEVELS], + minTick: 0, + maxTick: emptyMaxTick, + maxAmount: 0, + concentration: null, + } + } + + // Convert clearingPrice to decimal + const clearingPriceDecimal = toDecimal(clearingPrice, bidTokenInfo.decimals) + + // Rule 1 & 5: Use clearingPrice as minTick if it's lower than the lowest bid tick + let minTick = entries[0].tick + if (clearingPriceDecimal < minTick) { + minTick = clearingPriceDecimal + } + + const maxTickFromData = entries[entries.length - 1].tick + const maxAmount = Math.max(...entries.map((e) => e.amount)) + + // Rule 3 & 6 & 7: Calculate bar step and range + const { barStep, rangeMax, totalBars } = calculateBarStepAndRange({ + minTick, + maxTick: maxTickFromData, + tickSize: tickSizeDecimal, + }) + + // Rule 4: Calculate label increment for 10 labels + const labelIncrement = calculateLabelIncrement({ + minTick, + rangeMax, + tickSize: tickSizeDecimal, + }) + + // Build bars array - one bar per tick_size step + const bars: ChartBarData[] = [] + const bidLookup = new Map(entries.map((e) => [e.tick, e.amount])) + + for (let i = 0; i < totalBars; i++) { + const currentTick = minTick + i * barStep + + // Find exact match or very close match (within small tolerance for floating point) + const tolerance = barStep * TOLERANCE.TICK_COMPARISON + let matchedEntry = bidLookup.get(currentTick) + if (!matchedEntry) { + // Check for near matches + for (const [tick, amount] of bidLookup.entries()) { + if (Math.abs(tick - currentTick) < tolerance) { + matchedEntry = amount + break + } + } + } + + const displayValue = calculateTickDisplayValue({ + tickValue: currentTick, + displayMode, + bidTokenInfo, + totalSupply, + auctionTokenDecimals, + }) + + bars.push({ + tick: currentTick, + tickDisplay: formatter(displayValue), + amount: matchedEntry ?? 0, + index: i, + }) + } + + const yAxisLevels = calculateYAxisLevels(maxAmount) + + // Calculate bid concentration (only for bids at or above clearing price) + const concentration = calculateBidConcentration({ + bars: bars.map((bar) => ({ + tick: bar.tick, + amount: bar.amount, + index: bar.index, + })), + clearingPrice: clearingPriceDecimal, + }) + + return { + bars, + yAxisLevels, + minTick, + maxTick: rangeMax, + maxAmount, + labelIncrement, // Export for chart component to use + concentration, + } +} + +/** + * Calculate initial visible range for zoom functionality + * Shows clearing price (or minTick) at the left edge, plus INITIAL_TICK_COUNT ticks to the right + * + * @param params - Parameters including clearing price, tick size, and data range + * @returns Initial visible range in tick values { from, to } + */ +export function calculateInitialVisibleRange(params: { + clearingPrice: number + minTick: number + maxTick: number + tickSize: number + initialTickCount?: number +}): { from: number; to: number } { + const { clearingPrice, tickSize, initialTickCount = 20 } = params + + // Start from clearing price + const startTick = clearingPrice + + // Calculate end tick by adding initialTickCount ticks + const endTick = startTick + initialTickCount * tickSize + + // Make sure we don't exceed maxTick (but allow exceeding if needed to show initial count) + // The chart will handle empty bars beyond the data range + const finalEndTick = endTick + + return { + from: startTick, + to: Math.max(finalEndTick, startTick + tickSize), // Ensure at least one tick is shown + } +} + +/** + * Calculate label increment dynamically based on current visible range + * Reuses the existing calculateLabelIncrement logic but applies it to the visible range + */ +export function calculateDynamicLabelIncrement(params: { + visibleFrom: number + visibleTo: number + tickSize: number +}): number { + const { visibleFrom, visibleTo, tickSize } = params + + // Use the existing label increment calculation logic + return calculateLabelIncrement({ + minTick: visibleFrom, + rangeMax: visibleTo, + tickSize, + }) +} diff --git a/apps/web/src/components/Toucan/Auction/BidForm.tsx b/apps/web/src/components/Toucan/Auction/BidForm.tsx index 457f55a6c18..3a04ca9043e 100644 --- a/apps/web/src/components/Toucan/Auction/BidForm.tsx +++ b/apps/web/src/components/Toucan/Auction/BidForm.tsx @@ -28,6 +28,9 @@ import { buildCurrencyId } from 'uniswap/src/utils/currencyId' import { useEvent } from 'utilities/src/react/hooks' import { getDurationRemainingString } from 'utilities/src/time/duration' +// TODO(LP-335): replace these with actual ticks relative to the clearing price +const MAX_VALUATION_PRESETS = [1000000, 2000000, 3000000] + function useDurationRemaining(chainId: EVMUniverseChainId, endBlock: number | undefined) { const endBlockTimestamp = useBlockTimestamp({ chainId, @@ -67,21 +70,36 @@ function BidForm({ tokenColor, onBack }: { tokenColor?: ColorTokens; onBack: () const durationRemaining = useDurationRemaining(chainId as EVMUniverseChainId, endBlock) const bidCurrencyInfo = useCurrencyInfo(buildCurrencyId(chainId ?? UniverseChainId.Mainnet, bidTokenAddress ?? '')) - const [exactAmount, setExactAmount] = useState('') - const [isFiatMode, setIsFiatMode] = useState(false) - const currencyBalance = useCurrencyBalance(accountAddress, bidCurrencyInfo?.currency) - const currencyAmount = tryParseCurrencyAmount(exactAmount, bidCurrencyInfo?.currency) - const usdValue = useUSDCValue(currencyAmount) - const onToggleIsFiatMode = useEvent(() => { - setIsFiatMode((prev) => !prev) + // Budget input (top one) + const [exactBudgetAmount, setExactBudgetAmount] = useState('') + const [isBudgetFiatMode, setIsBudgetFiatMode] = useState(false) + const budgetCurrencyAmount = tryParseCurrencyAmount(exactBudgetAmount, bidCurrencyInfo?.currency) + const budgetUsdValue = useUSDCValue(budgetCurrencyAmount) + + const onToggleBudgetFiatMode = useEvent(() => { + setIsBudgetFiatMode((prev) => !prev) }) - const onSetPresetValue = useEvent((amount: string) => { + const onSetBudgetPresetValue = useEvent((amount: string) => { // When preset is selected, switch to token mode and set the amount - setIsFiatMode(false) - setExactAmount(amount) + setIsBudgetFiatMode(false) + setExactBudgetAmount(amount) + }) + + // Max valuation input (bottom one) + const [exactMaxValuationAmount, setExactMaxValuationAmount] = useState('') + const [isMaxValuationFiatMode, setIsMaxValuationFiatMode] = useState(false) + const maxValuationCurrencyAmount = tryParseCurrencyAmount(exactMaxValuationAmount, bidCurrencyInfo?.currency) + const maxValuationUsdValue = useUSDCValue(maxValuationCurrencyAmount) + + const onToggleValuationFiatMode = useEvent(() => { + setIsMaxValuationFiatMode((prev) => !prev) + }) + + const onSetPresetValuation = useEvent((value: number) => { + setExactMaxValuationAmount(value.toString()) }) return ( @@ -110,17 +128,36 @@ function BidForm({ tokenColor, onBack }: { tokenColor?: ColorTokens; onBack: () + state.currentBlockNumber) + + // Determine if we need polling based on auction state + const shouldPoll = useMemo((): boolean => { + if (!chainId || !endBlock) { + return false + } + + // First poll - we need to know current block + if (!currentBlockNumber) { + return true + } + + const current = Number(currentBlockNumber) + + // Auction ended - no need to poll + if (current > endBlock) { + return false + } + + // Auction active or upcoming - keep polling + return true + }, [chainId, endBlock, currentBlockNumber]) + + const { data: blockNumber } = useBlockNumber({ + chainId, + watch: shouldPoll, + query: { + enabled: shouldPoll, + }, + }) + + // biome-ignore lint/correctness/useExhaustiveDependencies: setCurrentBlockNumberAndUpdateProgress is stable from store actions + useEffect(() => { + if (blockNumber !== undefined) { + setCurrentBlockNumberAndUpdateProgress(blockNumber) + } + }, [blockNumber]) +} diff --git a/apps/web/src/components/Toucan/Auction/hooks/useBidTokenInfo.ts b/apps/web/src/components/Toucan/Auction/hooks/useBidTokenInfo.ts new file mode 100644 index 00000000000..b4a4af8f820 --- /dev/null +++ b/apps/web/src/components/Toucan/Auction/hooks/useBidTokenInfo.ts @@ -0,0 +1,63 @@ +import { BidTokenInfo } from 'components/Toucan/Auction/store/types' +import { useMemo } from 'react' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { useCurrencyInfoWithLoading } from 'uniswap/src/features/tokens/useCurrencyInfo' +import { useUSDCPrice } from 'uniswap/src/features/transactions/hooks/useUSDCPrice' +import { buildCurrencyId } from 'uniswap/src/utils/currencyId' + +/** + * Hook to fetch bid token information from on-chain/API data + * Derives decimals, symbol, and priceFiat (in USD) from bidTokenAddress and chainId + * Note: Multi-currency conversion is handled at the display layer + * + * @param bidTokenAddress - The address of the bid token (or undefined for native token) + * @param chainId - The chain ID where the token exists + * @returns BidTokenInfo with loading and error states + */ +export function useBidTokenInfo( + bidTokenAddress?: string, + chainId?: UniverseChainId, +): { bidTokenInfo: BidTokenInfo | undefined; loading: boolean; error?: Error } { + const currencyId = useMemo( + () => (chainId && bidTokenAddress ? buildCurrencyId(chainId, bidTokenAddress) : undefined), + [chainId, bidTokenAddress], + ) + const { currencyInfo, loading: currencyLoading, error: currencyError } = useCurrencyInfoWithLoading(currencyId) + const currency = currencyInfo?.currency + const { price, isLoading: isPriceLoading } = useUSDCPrice(currency) + + const bidTokenInfo = useMemo((): BidTokenInfo | undefined => { + if (!currency || !price) { + return undefined + } + + // Established pattern: parseFloat(price.toSignificant()) + // Used in packages/uniswap/src/features/transactions/swap/utils/trade.ts:150 + // NOTE | Toucan: Price is fetched in USD/stablecoin from on-chain data. + // Multi-currency support is handled at display layer via useFiatConverter. + // This ensures accurate blockchain prices with localized display formatting. + const priceFiat = parseFloat(price.toSignificant(6)) + if (isNaN(priceFiat)) { + return undefined + } + + return { + symbol: currency.symbol ?? 'UNKNOWN', // TODO | Toucan - handle undefined case + decimals: currency.decimals, + priceFiat, + } + }, [currency, price]) + + const priceParseError = useMemo(() => { + if (!currency || !price) { + return undefined + } + const priceFiat = parseFloat(price.toSignificant(6)) + return isNaN(priceFiat) ? new Error('Invalid token price: failed to parse price as a number') : undefined + }, [currency, price]) + + const loading = currencyLoading || !currencyInfo || isPriceLoading + const error = currencyError || priceParseError + + return { bidTokenInfo, loading, error } +} diff --git a/apps/web/src/components/Toucan/Auction/store/AuctionStoreContextProvider.tsx b/apps/web/src/components/Toucan/Auction/store/AuctionStoreContextProvider.tsx index dd80aaf5916..b3002f57b96 100644 --- a/apps/web/src/components/Toucan/Auction/store/AuctionStoreContextProvider.tsx +++ b/apps/web/src/components/Toucan/Auction/store/AuctionStoreContextProvider.tsx @@ -1,3 +1,4 @@ +import { useAuctionBlockPolling } from 'components/Toucan/Auction/hooks/useAuctionBlockPolling' import { AuctionStoreContext } from 'components/Toucan/Auction/store/AuctionStoreContext' import { createAuctionStore } from 'components/Toucan/Auction/store/createAuctionStore' import { useAuctionStore, useAuctionStoreActions } from 'components/Toucan/Auction/store/useAuctionStore' @@ -31,6 +32,13 @@ function useUpdateTokenColorInAuctionStore() { function AuctionStoreProviderInner({ children }: PropsWithChildren) { useUpdateTokenColorInAuctionStore() + const { chainId, endBlock } = useAuctionStore((state) => ({ + chainId: state.auctionDetails?.chainId, + endBlock: state.auctionDetails?.endBlock, + })) + + useAuctionBlockPolling(chainId, endBlock) + return children } diff --git a/apps/web/src/components/Toucan/Auction/store/createAuctionStore.ts b/apps/web/src/components/Toucan/Auction/store/createAuctionStore.ts index d3bd716a315..37a8e05c67a 100644 --- a/apps/web/src/components/Toucan/Auction/store/createAuctionStore.ts +++ b/apps/web/src/components/Toucan/Auction/store/createAuctionStore.ts @@ -1,5 +1,6 @@ import { FAKE_AUCTION_DATA, FAKE_CHECKPOINT_DATA } from 'components/Toucan/Auction/store/mockData' -import { AuctionStoreState, DisplayMode } from 'components/Toucan/Auction/store/types' +import { AuctionProgressState, AuctionStoreState, DisplayMode } from 'components/Toucan/Auction/store/types' +import { computeAuctionProgress } from 'components/Toucan/Auction/utils/computeAuctionProgress' import type { StoreApi, UseBoundStore } from 'zustand' import { create } from 'zustand' import { devtools } from 'zustand/middleware' @@ -15,6 +16,17 @@ export const createAuctionStore = (_auctionId?: string): AuctionStore => { checkpointData: FAKE_CHECKPOINT_DATA, tokenColor: undefined, // Will be set by useSrcColor in provider displayMode: DisplayMode.VALUATION, + currentBlockNumber: undefined, + progress: { + state: AuctionProgressState.NOT_STARTED, + blocksRemaining: undefined, + progressPercentage: undefined, + isGraduated: false, + }, + chartZoomState: { + visibleRange: null, + isZoomed: false, + }, // Actions actions: { @@ -24,6 +36,31 @@ export const createAuctionStore = (_auctionId?: string): AuctionStore => { setDisplayMode: (mode) => { set({ displayMode: mode }) }, + /** + * Updates the current block number and automatically recomputes all auction progress state. + * This will update progress.state, progress.blocksRemaining, progress.progressPercentage, and progress.isGraduated. + * @param blockNumber - The new current block number from the blockchain + */ + setCurrentBlockNumberAndUpdateProgress: (blockNumber) => { + set((state) => ({ + currentBlockNumber: blockNumber, + progress: computeAuctionProgress({ + currentBlock: blockNumber, + auctionDetails: state.auctionDetails, + }), + })) + }, + setChartZoomState: (state) => { + set({ chartZoomState: state }) + }, + resetChartZoom: () => { + set({ + chartZoomState: { + visibleRange: null, + isZoomed: false, + }, + }) + }, }, }), { diff --git a/apps/web/src/components/Toucan/Auction/store/mockData.ts b/apps/web/src/components/Toucan/Auction/store/mockData.ts index 57a5a9034e7..b6acd084b39 100644 --- a/apps/web/src/components/Toucan/Auction/store/mockData.ts +++ b/apps/web/src/components/Toucan/Auction/store/mockData.ts @@ -11,12 +11,95 @@ export const FAKE_AUCTION_DATA: AuctionDetails = { startBlock: 20525886, endBlock: 23525886, totalSupply: '1000000000000000000000000000', - tickSize: '10', graduationThreshold: 0.35, bidTokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC + tickSize: '500000', // $0.50 USDC + // TODO | Toucan: remove once token details are fetched using address + tokenDecimals: 18, } export const FAKE_CHECKPOINT_DATA: CheckpointData = { - clearingPrice: '5.00', + clearingPrice: '5000000', // $5 USDC cumulativeMps: 0.4, } + +export interface BidActivity { + walletAddress: string + bidVolume: string + price: string + timestamp: number // Unix timestamp in seconds +} + +export const FAKE_BID_ACTIVITIES: BidActivity[] = [ + { + walletAddress: '0x1234567890123456789012345678901234567890', + bidVolume: '100', + price: '2.5M', + timestamp: 1761263507, // Example unix timestamp + }, + { + walletAddress: '0x2345678901234567890123456789012345678901', + bidVolume: '250', + price: '2.8M', + timestamp: 1761263501, + }, + { + walletAddress: '0x3456789012345678901234567890123456789012', + bidVolume: '500', + price: '3.2M', + timestamp: 1761263500, + }, + { + walletAddress: '0x4567890123456789012345678901234567890123', + bidVolume: '100', + price: '2.5M', + timestamp: 1761263507, + }, + { + walletAddress: '0x5678901234567890123456789012345678901234', + bidVolume: '750', + price: '4.1M', + timestamp: 1761263507, + }, + { + walletAddress: '0x6789012345678901234567890123456789012345', + bidVolume: '100', + price: '2.5M', + timestamp: 1761263507, + }, +] + +export interface AuctionStatsData { + launchedBy: { + name: string + iconUrl?: string + } + launchedOn: string + contractAddress: string + description: string + website?: string + twitter?: string + impliedTokenPriceMin: string + impliedTokenPriceMax: string + totalBids: number + circulatingSupply: string + totalSupply: string +} + +export const FAKE_AUCTION_STATS: AuctionStatsData = { + launchedBy: { + name: 'FooCorp', + iconUrl: undefined, // Using placeholder in component until actual data is available + }, + launchedOn: '08/08/25', + contractAddress: '0x1234567890123456789012345678901234567890', + description: + 'FooCoin is an innovative token built on the Unichain blockchain, designed to empower users with unique features and functionalities. As a digital asset, it facilitates seamless transactions and interactions within the decentralized ecosystem, allowing holders to engage in various activities such as staking, trading, and participating in community governance. With its vibrant community and robust technology, FooCoin aims to enchant the crypto space and provide users with magical experiences.', + website: 'https://foocorp.example.com', + twitter: 'https://x.com/foocorp', + impliedTokenPriceMin: '$1M', + impliedTokenPriceMax: '$2.5M', + totalBids: 10000, + circulatingSupply: '1,000', + totalSupply: 'xx,xxx', +} diff --git a/apps/web/src/components/Toucan/Auction/store/mocks/distributionData/100_Ticks.ts b/apps/web/src/components/Toucan/Auction/store/mocks/distributionData/100_Ticks.ts new file mode 100644 index 00000000000..1a4dc6287c1 --- /dev/null +++ b/apps/web/src/components/Toucan/Auction/store/mocks/distributionData/100_Ticks.ts @@ -0,0 +1,111 @@ +import { FAKE_AUCTION_DATA } from 'components/Toucan/Auction/store/mockData' +import { BidDistributionData } from 'components/Toucan/Auction/store/types' + +const TICK_SIZE = FAKE_AUCTION_DATA.tickSize + +// 100 ticks - clearing price at $5.00 +// Realistic orderbook: very tight clustering above clearing price with exponential volume decay +export const MOCK_BID_DISTRIBUTION_DATA_100_TICKS: BidDistributionData = new Map([ + // Below clearing price - random distribution (30 ticks) + [(Number(TICK_SIZE) * 2).toString(), '150000000'], // $1.00 + [(Number(TICK_SIZE) * 3).toString(), '120000000'], // $1.50 + [(Number(TICK_SIZE) * 4).toString(), '180000000'], // $2.00 + [(Number(TICK_SIZE) * 5).toString(), '95000000'], // $2.50 + [(Number(TICK_SIZE) * 6).toString(), '140000000'], // $3.00 + [(Number(TICK_SIZE) * 7).toString(), '110000000'], // $3.50 + [(Number(TICK_SIZE) * 8).toString(), '160000000'], // $4.00 + [(Number(TICK_SIZE) * 9).toString(), '130000000'], // $4.50 + // At clearing price - highest volume + [(Number(TICK_SIZE) * 10).toString(), '2600000000'], // $5.00 - clearing price + // Above clearing - very tight clustering (70 ticks concentrated near clearing) + [(Number(TICK_SIZE) * 11).toString(), '2550000000'], // $5.50 + [(Number(TICK_SIZE) * 12).toString(), '2520000000'], // $6.00 + [(Number(TICK_SIZE) * 13).toString(), '2500000000'], // $6.50 + [(Number(TICK_SIZE) * 14).toString(), '2480000000'], // $7.00 + [(Number(TICK_SIZE) * 15).toString(), '2460000000'], // $7.50 + [(Number(TICK_SIZE) * 16).toString(), '2440000000'], // $8.00 + [(Number(TICK_SIZE) * 17).toString(), '2420000000'], // $8.50 + [(Number(TICK_SIZE) * 18).toString(), '2400000000'], // $9.00 + [(Number(TICK_SIZE) * 19).toString(), '2380000000'], // $9.50 + [(Number(TICK_SIZE) * 20).toString(), '2360000000'], // $10.00 + [(Number(TICK_SIZE) * 21).toString(), '2340000000'], // $10.50 + [(Number(TICK_SIZE) * 22).toString(), '2320000000'], // $11.00 + [(Number(TICK_SIZE) * 23).toString(), '2300000000'], // $11.50 + [(Number(TICK_SIZE) * 24).toString(), '2280000000'], // $12.00 + [(Number(TICK_SIZE) * 25).toString(), '2260000000'], // $12.50 + [(Number(TICK_SIZE) * 26).toString(), '2240000000'], // $13.00 + [(Number(TICK_SIZE) * 27).toString(), '2220000000'], // $13.50 + [(Number(TICK_SIZE) * 28).toString(), '2200000000'], // $14.00 + [(Number(TICK_SIZE) * 29).toString(), '2180000000'], // $14.50 + [(Number(TICK_SIZE) * 30).toString(), '2160000000'], // $15.00 + [(Number(TICK_SIZE) * 31).toString(), '2140000000'], // $15.50 + [(Number(TICK_SIZE) * 32).toString(), '2120000000'], // $16.00 + [(Number(TICK_SIZE) * 33).toString(), '2100000000'], // $16.50 + [(Number(TICK_SIZE) * 34).toString(), '2070000000'], // $17.00 + [(Number(TICK_SIZE) * 35).toString(), '2040000000'], // $17.50 + [(Number(TICK_SIZE) * 36).toString(), '2010000000'], // $18.00 + [(Number(TICK_SIZE) * 37).toString(), '1980000000'], // $18.50 + [(Number(TICK_SIZE) * 38).toString(), '1950000000'], // $19.00 + [(Number(TICK_SIZE) * 39).toString(), '1920000000'], // $19.50 + [(Number(TICK_SIZE) * 40).toString(), '1890000000'], // $20.00 + [(Number(TICK_SIZE) * 41).toString(), '1860000000'], // $20.50 + [(Number(TICK_SIZE) * 42).toString(), '1830000000'], // $21.00 + [(Number(TICK_SIZE) * 43).toString(), '1800000000'], // $21.50 + [(Number(TICK_SIZE) * 44).toString(), '1770000000'], // $22.00 + [(Number(TICK_SIZE) * 45).toString(), '1740000000'], // $22.50 + [(Number(TICK_SIZE) * 46).toString(), '1710000000'], // $23.00 + [(Number(TICK_SIZE) * 47).toString(), '1680000000'], // $23.50 + [(Number(TICK_SIZE) * 48).toString(), '1650000000'], // $24.00 + [(Number(TICK_SIZE) * 49).toString(), '1620000000'], // $24.50 + [(Number(TICK_SIZE) * 50).toString(), '1590000000'], // $25.00 + [(Number(TICK_SIZE) * 51).toString(), '1560000000'], // $25.50 + [(Number(TICK_SIZE) * 52).toString(), '1530000000'], // $26.00 + [(Number(TICK_SIZE) * 53).toString(), '1500000000'], // $26.50 + [(Number(TICK_SIZE) * 54).toString(), '1470000000'], // $27.00 + [(Number(TICK_SIZE) * 55).toString(), '1440000000'], // $27.50 + [(Number(TICK_SIZE) * 56).toString(), '1410000000'], // $28.00 + [(Number(TICK_SIZE) * 57).toString(), '1380000000'], // $28.50 + [(Number(TICK_SIZE) * 58).toString(), '1350000000'], // $29.00 + [(Number(TICK_SIZE) * 59).toString(), '1320000000'], // $29.50 + [(Number(TICK_SIZE) * 60).toString(), '1290000000'], // $30.00 + [(Number(TICK_SIZE) * 61).toString(), '1260000000'], // $30.50 + [(Number(TICK_SIZE) * 62).toString(), '1230000000'], // $31.00 + [(Number(TICK_SIZE) * 63).toString(), '1200000000'], // $31.50 + [(Number(TICK_SIZE) * 64).toString(), '1170000000'], // $32.00 + [(Number(TICK_SIZE) * 65).toString(), '1140000000'], // $32.50 + [(Number(TICK_SIZE) * 66).toString(), '1110000000'], // $33.00 + [(Number(TICK_SIZE) * 67).toString(), '1080000000'], // $33.50 + [(Number(TICK_SIZE) * 68).toString(), '1050000000'], // $34.00 + [(Number(TICK_SIZE) * 69).toString(), '1020000000'], // $34.50 + [(Number(TICK_SIZE) * 70).toString(), '990000000'], // $35.00 + [(Number(TICK_SIZE) * 71).toString(), '960000000'], // $35.50 + [(Number(TICK_SIZE) * 72).toString(), '930000000'], // $36.00 + [(Number(TICK_SIZE) * 73).toString(), '900000000'], // $36.50 + [(Number(TICK_SIZE) * 74).toString(), '870000000'], // $37.00 + [(Number(TICK_SIZE) * 75).toString(), '840000000'], // $37.50 + [(Number(TICK_SIZE) * 76).toString(), '810000000'], // $38.00 + [(Number(TICK_SIZE) * 77).toString(), '780000000'], // $38.50 + [(Number(TICK_SIZE) * 78).toString(), '750000000'], // $39.00 + [(Number(TICK_SIZE) * 79).toString(), '720000000'], // $39.50 + [(Number(TICK_SIZE) * 80).toString(), '690000000'], // $40.00 + [(Number(TICK_SIZE) * 82).toString(), '660000000'], // $41.00 + [(Number(TICK_SIZE) * 84).toString(), '630000000'], // $42.00 + [(Number(TICK_SIZE) * 86).toString(), '600000000'], // $43.00 + [(Number(TICK_SIZE) * 88).toString(), '570000000'], // $44.00 + [(Number(TICK_SIZE) * 90).toString(), '540000000'], // $45.00 + [(Number(TICK_SIZE) * 92).toString(), '510000000'], // $46.00 + [(Number(TICK_SIZE) * 94).toString(), '480000000'], // $47.00 + [(Number(TICK_SIZE) * 96).toString(), '450000000'], // $48.00 + [(Number(TICK_SIZE) * 98).toString(), '420000000'], // $49.00 + [(Number(TICK_SIZE) * 100).toString(), '390000000'], // $50.00 + [(Number(TICK_SIZE) * 105).toString(), '350000000'], // $52.50 + [(Number(TICK_SIZE) * 110).toString(), '320000000'], // $55.00 + [(Number(TICK_SIZE) * 115).toString(), '290000000'], // $57.50 + [(Number(TICK_SIZE) * 120).toString(), '260000000'], // $60.00 + // Outliers - rare bids far from clearing + [(Number(TICK_SIZE) * 140).toString(), '220000000'], // $70.00 + [(Number(TICK_SIZE) * 160).toString(), '180000000'], // $80.00 + [(Number(TICK_SIZE) * 200).toString(), '140000000'], // $100.00 + [(Number(TICK_SIZE) * 250).toString(), '100000000'], // $125.00 + [(Number(TICK_SIZE) * 300).toString(), '80000000'], // $150.00 +]) diff --git a/apps/web/src/components/Toucan/Auction/store/mocks/distributionData/10_Ticks.ts b/apps/web/src/components/Toucan/Auction/store/mocks/distributionData/10_Ticks.ts new file mode 100644 index 00000000000..df5463983ee --- /dev/null +++ b/apps/web/src/components/Toucan/Auction/store/mocks/distributionData/10_Ticks.ts @@ -0,0 +1,21 @@ +import { FAKE_AUCTION_DATA } from 'components/Toucan/Auction/store/mockData' +import { BidDistributionData } from 'components/Toucan/Auction/store/types' + +const TICK_SIZE = FAKE_AUCTION_DATA.tickSize + +// 10 ticks clustered near clearing price ($5.00) +// Realistic orderbook: tight clustering above clearing, descending volume with distance +export const MOCK_BID_DISTRIBUTION_DATA_10_TICKS: BidDistributionData = new Map([ + // Below clearing price - random distribution (30%) + [(Number(TICK_SIZE) * 2).toString(), '420000000'], // $1.00 + [(Number(TICK_SIZE) * 6).toString(), '380000000'], // $3.00 + [(Number(TICK_SIZE) * 8).toString(), '550000000'], // $4.00 + // At and above clearing price - tight clustering with descending volume (70%) + [(Number(TICK_SIZE) * 10).toString(), '920000000'], // $5.00 - clearing price (highest volume) + [(Number(TICK_SIZE) * 11).toString(), '880000000'], // $5.50 + [(Number(TICK_SIZE) * 12).toString(), '830000000'], // $6.00 + [(Number(TICK_SIZE) * 13).toString(), '740000000'], // $6.50 + [(Number(TICK_SIZE) * 14).toString(), '650000000'], // $7.00 + [(Number(TICK_SIZE) * 15).toString(), '610000000'], // $7.50 + [(Number(TICK_SIZE) * 18).toString(), '470000000'], // $9.00 - outlier +]) diff --git a/apps/web/src/components/Toucan/Auction/store/mocks/distributionData/20_Ticks.ts b/apps/web/src/components/Toucan/Auction/store/mocks/distributionData/20_Ticks.ts new file mode 100644 index 00000000000..fe31d23532f --- /dev/null +++ b/apps/web/src/components/Toucan/Auction/store/mocks/distributionData/20_Ticks.ts @@ -0,0 +1,31 @@ +import { FAKE_AUCTION_DATA } from 'components/Toucan/Auction/store/mockData' +import { BidDistributionData } from 'components/Toucan/Auction/store/types' + +const TICK_SIZE = FAKE_AUCTION_DATA.tickSize + +// 20 ticks - clearing price at $5.00 +// Realistic orderbook: tight clustering above clearing price with descending volume +export const MOCK_BID_DISTRIBUTION_DATA_20_TICKS: BidDistributionData = new Map([ + // Below clearing price - random distribution (30%) + [(Number(TICK_SIZE) * 2).toString(), '20000000000'], // $1.00 + [(Number(TICK_SIZE) * 4).toString(), '15000000000'], // $2.00 + [(Number(TICK_SIZE) * 6).toString(), '20000000000'], // $3.00 + [(Number(TICK_SIZE) * 7).toString(), '10000000000'], // $3.50 + [(Number(TICK_SIZE) * 8).toString(), '20000000000'], // $4.00 + [(Number(TICK_SIZE) * 9).toString(), '18000000000'], // $4.50 + // At and above clearing price - tight clustering with descending volume (70%) + [(Number(TICK_SIZE) * 10).toString(), '65000000000'], // $5.00 - clearing price (highest volume) + [(Number(TICK_SIZE) * 11).toString(), '50000000000'], // $5.50 + [(Number(TICK_SIZE) * 12).toString(), '50000000000'], // $6.00 + [(Number(TICK_SIZE) * 13).toString(), '45000000000'], // $6.50 + [(Number(TICK_SIZE) * 14).toString(), '45000000000'], // $7.00 + [(Number(TICK_SIZE) * 15).toString(), '40000000000'], // $7.50 + [(Number(TICK_SIZE) * 16).toString(), '35000000000'], // $8.00 + [(Number(TICK_SIZE) * 17).toString(), '30000000000'], // $8.50 + [(Number(TICK_SIZE) * 18).toString(), '25000000000'], // $9.00 + [(Number(TICK_SIZE) * 19).toString(), '20000000000'], // $9.50 + [(Number(TICK_SIZE) * 20).toString(), '15000000000'], // $10.00 + [(Number(TICK_SIZE) * 22).toString(), '10000000000'], // $11.00 + [(Number(TICK_SIZE) * 24).toString(), '5000000000'], // $12.00 + [(Number(TICK_SIZE) * 30).toString(), '1000000000'], // $15.00 - outlier +]) diff --git a/apps/web/src/components/Toucan/Auction/store/mocks/distributionData/50_Ticks.ts b/apps/web/src/components/Toucan/Auction/store/mocks/distributionData/50_Ticks.ts new file mode 100644 index 00000000000..805098a8f6e --- /dev/null +++ b/apps/web/src/components/Toucan/Auction/store/mocks/distributionData/50_Ticks.ts @@ -0,0 +1,62 @@ +import { FAKE_AUCTION_DATA } from 'components/Toucan/Auction/store/mockData' +import { BidDistributionData } from 'components/Toucan/Auction/store/types' + +const TICK_SIZE = FAKE_AUCTION_DATA.tickSize + +// 50 ticks - clearing price at $5.00 +// Realistic orderbook: tight clustering above clearing price with exponential decay in volume +export const MOCK_BID_DISTRIBUTION_DATA_50_TICKS: BidDistributionData = new Map([ + // Below clearing price - random distribution (30%) + [(Number(TICK_SIZE) * 2).toString(), '120000000'], // $1.00 + [(Number(TICK_SIZE) * 3).toString(), '95000000'], // $1.50 + [(Number(TICK_SIZE) * 4).toString(), '110000000'], // $2.00 + [(Number(TICK_SIZE) * 5).toString(), '80000000'], // $2.50 + [(Number(TICK_SIZE) * 6).toString(), '90000000'], // $3.00 + [(Number(TICK_SIZE) * 7).toString(), '85000000'], // $3.50 + [(Number(TICK_SIZE) * 8).toString(), '100000000'], // $4.00 + [(Number(TICK_SIZE) * 9).toString(), '95000000'], // $4.50 + // At clearing price - highest volume + [(Number(TICK_SIZE) * 10).toString(), '2800000000'], // $5.00 - clearing price + // Above clearing price - tight clustering with descending volume + [(Number(TICK_SIZE) * 11).toString(), '2700000000'], // $5.50 + [(Number(TICK_SIZE) * 12).toString(), '2650000000'], // $6.00 + [(Number(TICK_SIZE) * 13).toString(), '2600000000'], // $6.50 + [(Number(TICK_SIZE) * 14).toString(), '2550000000'], // $7.00 + [(Number(TICK_SIZE) * 15).toString(), '2500000000'], // $7.50 + [(Number(TICK_SIZE) * 16).toString(), '2450000000'], // $8.00 + [(Number(TICK_SIZE) * 17).toString(), '2400000000'], // $8.50 + [(Number(TICK_SIZE) * 18).toString(), '2350000000'], // $9.00 + [(Number(TICK_SIZE) * 19).toString(), '2300000000'], // $9.50 + [(Number(TICK_SIZE) * 20).toString(), '2250000000'], // $10.00 + [(Number(TICK_SIZE) * 21).toString(), '2200000000'], // $10.50 + [(Number(TICK_SIZE) * 22).toString(), '2150000000'], // $11.00 + [(Number(TICK_SIZE) * 23).toString(), '2100000000'], // $11.50 + [(Number(TICK_SIZE) * 24).toString(), '2050000000'], // $12.00 + [(Number(TICK_SIZE) * 25).toString(), '1950000000'], // $12.50 + [(Number(TICK_SIZE) * 26).toString(), '1850000000'], // $13.00 + [(Number(TICK_SIZE) * 27).toString(), '1750000000'], // $13.50 + [(Number(TICK_SIZE) * 28).toString(), '1650000000'], // $14.00 + [(Number(TICK_SIZE) * 29).toString(), '1550000000'], // $14.50 + [(Number(TICK_SIZE) * 30).toString(), '1450000000'], // $15.00 + [(Number(TICK_SIZE) * 31).toString(), '1350000000'], // $15.50 + [(Number(TICK_SIZE) * 32).toString(), '1250000000'], // $16.00 + [(Number(TICK_SIZE) * 33).toString(), '1150000000'], // $16.50 + [(Number(TICK_SIZE) * 34).toString(), '1050000000'], // $17.00 + [(Number(TICK_SIZE) * 35).toString(), '950000000'], // $17.50 + [(Number(TICK_SIZE) * 36).toString(), '850000000'], // $18.00 + [(Number(TICK_SIZE) * 37).toString(), '750000000'], // $18.50 + [(Number(TICK_SIZE) * 38).toString(), '650000000'], // $19.00 + [(Number(TICK_SIZE) * 40).toString(), '550000000'], // $20.00 + [(Number(TICK_SIZE) * 42).toString(), '450000000'], // $21.00 + [(Number(TICK_SIZE) * 44).toString(), '380000000'], // $22.00 + [(Number(TICK_SIZE) * 46).toString(), '320000000'], // $23.00 + [(Number(TICK_SIZE) * 48).toString(), '280000000'], // $24.00 + [(Number(TICK_SIZE) * 52).toString(), '240000000'], // $26.00 + [(Number(TICK_SIZE) * 56).toString(), '200000000'], // $28.00 + [(Number(TICK_SIZE) * 60).toString(), '180000000'], // $30.00 + // Outliers + [(Number(TICK_SIZE) * 70).toString(), '150000000'], // $35.00 + [(Number(TICK_SIZE) * 90).toString(), '120000000'], // $45.00 + [(Number(TICK_SIZE) * 120).toString(), '100000000'], // $60.00 + [(Number(TICK_SIZE) * 180).toString(), '80000000'], // $90.00 +]) diff --git a/apps/web/src/components/Toucan/Auction/store/mocks/distributionData/bidDistributionMockData.ts b/apps/web/src/components/Toucan/Auction/store/mocks/distributionData/bidDistributionMockData.ts new file mode 100644 index 00000000000..3151dc49e33 --- /dev/null +++ b/apps/web/src/components/Toucan/Auction/store/mocks/distributionData/bidDistributionMockData.ts @@ -0,0 +1,15 @@ +// TODO | Toucan: Remove this file once live auction data is implemented +// This file contains mock data for testing the BidDistributionChart with different data sets + +import { MOCK_BID_DISTRIBUTION_DATA_10_TICKS } from 'components/Toucan/Auction/store/mocks/distributionData/10_Ticks' +import { MOCK_BID_DISTRIBUTION_DATA_20_TICKS } from 'components/Toucan/Auction/store/mocks/distributionData/20_Ticks' +import { MOCK_BID_DISTRIBUTION_DATA_50_TICKS } from 'components/Toucan/Auction/store/mocks/distributionData/50_Ticks' +import { MOCK_BID_DISTRIBUTION_DATA_100_TICKS } from 'components/Toucan/Auction/store/mocks/distributionData/100_Ticks' +import { BidDistributionData } from 'components/Toucan/Auction/store/types' + +export const MOCK_BID_DISTRIBUTION_DATASETS: BidDistributionData[] = [ + MOCK_BID_DISTRIBUTION_DATA_10_TICKS, + MOCK_BID_DISTRIBUTION_DATA_20_TICKS, + MOCK_BID_DISTRIBUTION_DATA_50_TICKS, + MOCK_BID_DISTRIBUTION_DATA_100_TICKS, +] diff --git a/apps/web/src/components/Toucan/Auction/store/mocks/useMockDataStore.ts b/apps/web/src/components/Toucan/Auction/store/mocks/useMockDataStore.ts new file mode 100644 index 00000000000..98c06b1d352 --- /dev/null +++ b/apps/web/src/components/Toucan/Auction/store/mocks/useMockDataStore.ts @@ -0,0 +1,97 @@ +// TODO | Toucan: Remove this file once live auction data is implemented +// Temporary zustand store for selecting mock bid distribution data in development + +import { SavedCustomPreset } from 'components/Toucan/Auction/BidDistributionChart/dev/customPresets' +import { MOCK_BID_DISTRIBUTION_DATASETS } from 'components/Toucan/Auction/store/mocks/distributionData/bidDistributionMockData' +import { BidDistributionData } from 'components/Toucan/Auction/store/types' +import { create } from 'zustand' + +interface MockDataState { + // Dataset selection + selectedDatasetIndex: number + selectedDataset: BidDistributionData + isCustomPreset: boolean + selectedPresetId: string | null // Track which custom preset is selected + setSelectedDatasetIndex: (index: number) => void + + // Test parameters (for customization) + bidTokenAddress: string + tickSize: string + clearingPrice: string + totalSupply: string + setTestParameters: (params: { + bidTokenAddress: string + tickSize: string + clearingPrice: string + totalSupply: string + }) => void + + // Custom preset management + loadCustomPreset: (preset: SavedCustomPreset) => void + resetToDefaults: () => void +} + +// Default values from FAKE_AUCTION_DATA +const DEFAULT_BID_TOKEN_ADDRESS = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' // USDC +const DEFAULT_TICK_SIZE = '500000' // 0.50 USDC +const DEFAULT_CLEARING_PRICE = '5000000' // 5.00 USDC +const DEFAULT_TOTAL_SUPPLY = '1000000000000000000000000000' // 1B tokens with 18 decimals + +export const useMockDataStore = create((set) => ({ + // Dataset selection + selectedDatasetIndex: 0, + selectedDataset: MOCK_BID_DISTRIBUTION_DATASETS[0], + isCustomPreset: false, + selectedPresetId: null, + setSelectedDatasetIndex: (index: number) => + set({ + selectedDatasetIndex: index, + selectedDataset: MOCK_BID_DISTRIBUTION_DATASETS[index], + isCustomPreset: false, + selectedPresetId: null, // Clear preset ID when selecting quick preset + // Reset ALL test parameters to defaults when selecting quick presets + bidTokenAddress: DEFAULT_BID_TOKEN_ADDRESS, + tickSize: DEFAULT_TICK_SIZE, + clearingPrice: DEFAULT_CLEARING_PRICE, + totalSupply: DEFAULT_TOTAL_SUPPLY, + }), + + // Test parameters + bidTokenAddress: DEFAULT_BID_TOKEN_ADDRESS, + tickSize: DEFAULT_TICK_SIZE, + clearingPrice: DEFAULT_CLEARING_PRICE, + totalSupply: DEFAULT_TOTAL_SUPPLY, + setTestParameters: (params) => + set({ + bidTokenAddress: params.bidTokenAddress, + tickSize: params.tickSize, + clearingPrice: params.clearingPrice, + totalSupply: params.totalSupply, + }), + + // Custom preset management + loadCustomPreset: (preset) => { + set({ + selectedDataset: preset.distributionData, + selectedDatasetIndex: -1, // Indicates custom preset + isCustomPreset: true, + selectedPresetId: preset.id, // Store the specific preset ID + bidTokenAddress: preset.bidTokenAddress, // Load actual token address from preset + tickSize: preset.tickSize, + clearingPrice: preset.clearingPrice, + totalSupply: preset.totalSupply, + }) + }, + + resetToDefaults: () => + set({ + selectedDatasetIndex: 0, + selectedDataset: MOCK_BID_DISTRIBUTION_DATASETS[0], + isCustomPreset: false, + selectedPresetId: null, + bidTokenAddress: DEFAULT_BID_TOKEN_ADDRESS, + tickSize: DEFAULT_TICK_SIZE, + clearingPrice: DEFAULT_CLEARING_PRICE, + totalSupply: DEFAULT_TOTAL_SUPPLY, + }), +})) diff --git a/apps/web/src/components/Toucan/Auction/store/types.ts b/apps/web/src/components/Toucan/Auction/store/types.ts index d6dcbf3f164..f76c9a9199c 100644 --- a/apps/web/src/components/Toucan/Auction/store/types.ts +++ b/apps/web/src/components/Toucan/Auction/store/types.ts @@ -1,9 +1,11 @@ -import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { EVMUniverseChainId } from 'uniswap/src/features/chains/types' + +export type BidDistributionData = Map // potentially missing clearing price export interface AuctionDetails { auctionId: string - chainId: UniverseChainId + chainId: EVMUniverseChainId tokenSymbol: string tokenAddress: string tokenName: string // this is missing from what Rob sent @@ -15,6 +17,8 @@ export interface AuctionDetails { tickSize: string graduationThreshold: number bidTokenAddress: string + // TODO | Toucan: remove once token details are fetched using address + tokenDecimals: number // Token decimals for totalSupply conversion } export interface CheckpointData { @@ -27,16 +31,58 @@ export enum DisplayMode { TOKEN_PRICE = 'TOKEN_PRICE', } +export enum AuctionProgressState { + NOT_STARTED = 'NOT_STARTED', + IN_PROGRESS = 'IN_PROGRESS', + ENDED = 'ENDED', +} + +/** + * Computed auction progress information + * These fields are automatically updated when currentBlockNumber changes + */ +export interface AuctionProgressData { + state: AuctionProgressState + blocksRemaining: number | undefined + progressPercentage: number | undefined + isGraduated: boolean +} + +// TODO | Toucan - determine if this can be replaced with SDK Token type +/** + * Bid token metadata used for chart calculations + * Note: priceFiat is fetched in USD from on-chain stablecoin data. + * Multi-currency display is handled at the component layer via useFiatConverter. + */ +export interface BidTokenInfo { + symbol: string + decimals: number + /** Token price in USD - converted to user's selected fiat currency at display time */ + priceFiat: number +} + +// Chart zoom state for tracking visible range and zoom status +interface ChartZoomState { + visibleRange: { from: number; to: number } | null + isZoomed: boolean +} + interface AuctionState { auctionDetails: AuctionDetails | null checkpointData: CheckpointData | null tokenColor?: string displayMode: DisplayMode + currentBlockNumber: bigint | undefined + progress: AuctionProgressData + chartZoomState: ChartZoomState } interface AuctionActions { setTokenColor: (color?: string) => void setDisplayMode: (mode: DisplayMode) => void + setCurrentBlockNumberAndUpdateProgress: (blockNumber: bigint | undefined) => void + setChartZoomState: (state: ChartZoomState) => void + resetChartZoom: () => void } export type AuctionStoreState = AuctionState & { diff --git a/apps/web/src/components/Toucan/Auction/utils/computeAuctionProgress.ts b/apps/web/src/components/Toucan/Auction/utils/computeAuctionProgress.ts new file mode 100644 index 00000000000..151be43bfc2 --- /dev/null +++ b/apps/web/src/components/Toucan/Auction/utils/computeAuctionProgress.ts @@ -0,0 +1,76 @@ +import type { AuctionDetails, AuctionProgressData } from 'components/Toucan/Auction/store/types' +import { AuctionProgressState } from 'components/Toucan/Auction/store/types' + +function getAuctionProgressState({ + currentBlock, + startBlock, + endBlock, +}: { + currentBlock: bigint | number | undefined + startBlock: number | undefined + endBlock: number | undefined +}): AuctionProgressState { + if (!currentBlock || !startBlock || !endBlock) { + return AuctionProgressState.NOT_STARTED + } + + const current = typeof currentBlock === 'bigint' ? Number(currentBlock) : currentBlock + + if (current < startBlock) { + return AuctionProgressState.NOT_STARTED + } + + if (current > endBlock) { + return AuctionProgressState.ENDED + } + + return AuctionProgressState.IN_PROGRESS +} + +/** + * Computes all auction progress information from current block and auction details + * This is a pure function that can be tested independently of the store + * @param params - Object containing current block and auction details + * @param params.currentBlock - The current block number + * @param params.auctionDetails - The auction details containing start/end blocks + * @returns Computed auction progress state and derived values + */ +export function computeAuctionProgress({ + currentBlock, + auctionDetails, +}: { + currentBlock: bigint | undefined + auctionDetails: AuctionDetails | null +}): AuctionProgressData { + const state = getAuctionProgressState({ + currentBlock, + startBlock: auctionDetails?.startBlock, + endBlock: auctionDetails?.endBlock, + }) + + let blocksRemaining: number | undefined + let progressPercentage: number | undefined + + if (currentBlock && auctionDetails) { + const current = Number(currentBlock) + const { startBlock, endBlock } = auctionDetails + + if (state === AuctionProgressState.IN_PROGRESS) { + blocksRemaining = endBlock - current + const totalBlocks = endBlock - startBlock + const elapsedBlocks = current - startBlock + // TODO | Toucan: if progress is percentage sold rather than blocks passed, this needs to be updated + progressPercentage = totalBlocks > 0 ? Math.min(100, (elapsedBlocks / totalBlocks) * 100) : 0 + } + } + + // TODO | Toucan: update with graduation logic once schema from back end has been decided on + const isGraduated = false + + return { + state, + blocksRemaining, + progressPercentage, + isGraduated, + } +} diff --git a/apps/web/src/components/Toucan/Shared/ToucanContainer.tsx b/apps/web/src/components/Toucan/Shared/ToucanContainer.tsx index 1eedcd3c335..c69b52f7d3f 100644 --- a/apps/web/src/components/Toucan/Shared/ToucanContainer.tsx +++ b/apps/web/src/components/Toucan/Shared/ToucanContainer.tsx @@ -1,11 +1,11 @@ import { ComponentProps, PropsWithChildren } from 'react' import { Flex } from 'ui/src' -const CONTAINER_MAX_WIDTH = 1200 +const CONTAINER_WIDTH = 1200 export const ToucanContainer = ({ children, ...props }: PropsWithChildren>) => { return ( - + {children} ) diff --git a/apps/web/src/components/Toucan/TopAuctionsTable.tsx b/apps/web/src/components/Toucan/TopAuctionsTable.tsx new file mode 100644 index 00000000000..e5fb6feb787 --- /dev/null +++ b/apps/web/src/components/Toucan/TopAuctionsTable.tsx @@ -0,0 +1,118 @@ +import { createColumnHelper } from '@tanstack/react-table' +import { Table } from 'components/Table' +import { Cell } from 'components/Table/Cell' +import { EllipsisText, HeaderCell, TableText } from 'components/Table/styled' +import { MAX_WIDTH_MEDIA_BREAKPOINT } from 'components/Tokens/constants' +import useSimplePagination from 'hooks/useSimplePagination' +import { memo, ReactElement, useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { TABLE_PAGE_SIZE } from 'state/explore' +import { useTopAuctions } from 'state/explore/topAuctions' +import { Flex, styled, Text, useMedia } from 'ui/src' +import { Auction } from 'uniswap/src/data/rest/auctions/types' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' + +const TableWrapper = styled(Flex, { + m: '0 auto', + maxWidth: MAX_WIDTH_MEDIA_BREAKPOINT, +}) + +interface TopAuctionsTableValue { + index: number + tokenName: ReactElement + link: string +} + +function TokenNameCell({ auction }: { auction: Auction }) { + return ( + + {auction.token_name} + + {auction.token_symbol} + + + ) +} + +export const ToucanTable = memo(function ToucanTable() { + const { topAuctions, isLoading, isError } = useTopAuctions() + + const { page, loadMore } = useSimplePagination() + + return ( + + + + ) +}) + +function ToucanTableComponent({ + auctions, + loading, + error, + loadMore, +}: { + auctions?: readonly Auction[] + loading: boolean + error?: boolean + loadMore?: ({ onComplete }: { onComplete?: () => void }) => void +}) { + const { t } = useTranslation() + const topAuctionsTableValues: TopAuctionsTableValue[] | undefined = useMemo( + () => + auctions?.map((auction, i) => { + const chainUrlParam = getChainInfo(auction.chain_id).urlParam + return { + index: i + 1, + tokenName: , + link: `/explore/auctions/${chainUrlParam}/${auction.auction_id}`, + } + }) ?? [], + [auctions], + ) + + const showLoadingSkeleton = loading || !!error + + const media = useMedia() + const columns = useMemo(() => { + const columnHelper = createColumnHelper() + const filteredColumns = [ + columnHelper.accessor((row) => row.tokenName, { + id: 'tokenName', + size: media.lg ? 150 : 300, + header: () => ( + + + {t('common.tokenName')} + + + ), + cell: (auctionDescription) => ( + + {auctionDescription.getValue()} + + ), + }), + ] + + return filteredColumns.filter((column): column is NonNullable<(typeof filteredColumns)[number]> => Boolean(column)) + }, [showLoadingSkeleton, media, t]) + + return ( +
+ ) +} diff --git a/apps/web/src/components/V2Unsupported.tsx b/apps/web/src/components/V2Unsupported.tsx index dbd6b3912ec..6f072cc114d 100644 --- a/apps/web/src/components/V2Unsupported.tsx +++ b/apps/web/src/components/V2Unsupported.tsx @@ -1,9 +1,9 @@ import { AutoColumn } from 'components/deprecated/Column' -import styled from 'lib/styled-components' +import { deprecatedStyled } from 'lib/styled-components' import { Trans } from 'react-i18next' import { ThemedText } from 'theme/components' -const TextWrapper = styled.div` +const TextWrapper = deprecatedStyled.div` border: 1px solid ${({ theme }) => theme.neutral3}; padding: 16px 12px; border-radius: 12px; diff --git a/apps/web/src/components/WalletModal/PrivacyPolicyNotice.tsx b/apps/web/src/components/WalletModal/PrivacyPolicyNotice.tsx index c88887e3d93..39c50789ee9 100644 --- a/apps/web/src/components/WalletModal/PrivacyPolicyNotice.tsx +++ b/apps/web/src/components/WalletModal/PrivacyPolicyNotice.tsx @@ -1,9 +1,9 @@ -import styled from 'lib/styled-components' +import { deprecatedStyled } from 'lib/styled-components' import { Trans } from 'react-i18next' import { ExternalLink } from 'theme/components/Links' import { Text } from 'ui/src' -const StyledLink = styled(ExternalLink)` +const StyledLink = deprecatedStyled(ExternalLink)` font-weight: 535; color: ${({ theme }) => theme.neutral2}; ` diff --git a/apps/web/src/components/WalletModal/UniswapWalletOptions.tsx b/apps/web/src/components/WalletModal/UniswapWalletOptions.tsx index 1ef39364689..ba0017222ef 100644 --- a/apps/web/src/components/WalletModal/UniswapWalletOptions.tsx +++ b/apps/web/src/components/WalletModal/UniswapWalletOptions.tsx @@ -42,7 +42,7 @@ export function OptionContainer({ hideBackground, recent, children, onPress, tes maxHeight={72} cursor="pointer" zIndex="$default" - backgroundColor={!hideBackground ? '$surface2' : undefined} + backgroundColor={!hideBackground ? '$surface2' : '$transparent'} hoverStyle={{ backgroundColor: '$surface3' }} onPress={onPress} data-testid={testID} diff --git a/apps/web/src/components/WalletModal/__snapshots__/UniswapWalletOptions.test.tsx.snap b/apps/web/src/components/WalletModal/__snapshots__/UniswapWalletOptions.test.tsx.snap index 08ee3ac3565..ecc5bb999e2 100644 --- a/apps/web/src/components/WalletModal/__snapshots__/UniswapWalletOptions.test.tsx.snap +++ b/apps/web/src/components/WalletModal/__snapshots__/UniswapWalletOptions.test.tsx.snap @@ -17,11 +17,10 @@ exports[`UniswapWalletOptions > Download wallet option should be visible if exte data-testid="download-uniswap-wallet" >
) => { - const theme = useTheme() - const bg = theme.darkMode ? 'black' : 'white' + const isDarkMode = useIsDarkMode() + const bg = isDarkMode ? 'black' : 'white' return ( diff --git a/apps/web/src/components/Web3Provider/TestWeb3Provider.tsx b/apps/web/src/components/Web3Provider/TestWeb3Provider.tsx index 687c7cde91f..04d72142aad 100644 --- a/apps/web/src/components/Web3Provider/TestWeb3Provider.tsx +++ b/apps/web/src/components/Web3Provider/TestWeb3Provider.tsx @@ -11,7 +11,6 @@ import { wagmiConfig } from 'components/Web3Provider/wagmiConfig' const TestWeb3Provider = createWeb3Provider({ wagmiConfig, reconnectOnMount: false, - includeCapabilitiesEffects: false, }) export default TestWeb3Provider diff --git a/apps/web/src/components/Web3Provider/WebUniswapContext.tsx b/apps/web/src/components/Web3Provider/WebUniswapContext.tsx index cc1f14fbffa..624b206f311 100644 --- a/apps/web/src/components/Web3Provider/WebUniswapContext.tsx +++ b/apps/web/src/components/Web3Provider/WebUniswapContext.tsx @@ -62,7 +62,7 @@ function WebUniswapProviderInner({ children }: PropsWithChildren) { const location = useLocation() const accountDrawer = useAccountDrawer() const navigate = useNavigate() - const navigateToFiatOnRamp = useCallback(() => navigate(`/buy`, { replace: true }), [navigate]) + const navigateToFiatOnRamp = useCallback(() => navigate(`/buy`), [navigate]) const { closeModal: closeSearchModal } = useModalState(ModalName.Search) const { openModal: openSendModal } = useModalState(ModalName.Send) @@ -75,7 +75,7 @@ function WebUniswapProviderInner({ children }: PropsWithChildren) { chainId: inputCurrencyId ? currencyIdToChain(inputCurrencyId) : undefined, outputChainId: outputCurrencyId ? currencyIdToChain(outputCurrencyId) : undefined, }) - navigate(`/swap${queryParams}`, { replace: true }) + navigate(`/swap${queryParams}`) closeSearchModal() accountDrawer.close() }, @@ -96,15 +96,12 @@ function WebUniswapProviderInner({ children }: PropsWithChildren) { const chainUrlParam = getChainInfo(chainId).urlParam openSendModal() closeSearchModal() - accountDrawer.close() const newPathname = location.pathname === '/' ? '/send' : location.pathname const currencyAddressParam = currencyAddress ? `&sendCurrency=${currencyAddress}` : '' - navigate(`${newPathname}?sendChain=${chainUrlParam}${currencyAddressParam}`, { - replace: true, - }) + navigate(`${newPathname}?sendChain=${chainUrlParam}${currencyAddressParam}`) }, - [openSendModal, closeSearchModal, accountDrawer, navigate, location], + [openSendModal, closeSearchModal, navigate, location], ) const navigateToReceive = useOpenReceiveCryptoModal({ @@ -139,8 +136,14 @@ function WebUniswapProviderInner({ children }: PropsWithChildren) { }) const getCanSignPermits = useGetCanSignPermits() - // no-op until we have an external profile screen on web - const navigateToExternalProfile = useCallback((_: { address: Address }) => noop(), []) + const navigateToExternalProfile = useCallback( + ({ address }: { address: Address }) => { + // TODO: this will need to be updated upstack + navigate(`/portfolio?address=${address}`) + closeSearchModal() + }, + [navigate, closeSearchModal], + ) const navigateToNftCollection = useCallback((args: { collectionAddress: Address; chainId: UniverseChainId }) => { window.open( diff --git a/apps/web/src/components/Web3Provider/createWeb3Provider.tsx b/apps/web/src/components/Web3Provider/createWeb3Provider.tsx index 295cab35401..11346af0509 100644 --- a/apps/web/src/components/Web3Provider/createWeb3Provider.tsx +++ b/apps/web/src/components/Web3Provider/createWeb3Provider.tsx @@ -1,26 +1,16 @@ import { CoinbaseWalletAdapter } from '@solana/wallet-adapter-coinbase' import { WalletProvider as SolanaWalletProvider } from '@solana/wallet-adapter-react' import { SolanaSignerUpdater } from 'components/Web3Provider/signSolanaTransaction' -import React, { PropsWithChildren, ReactNode, useMemo } from 'react' +import React, { type PropsWithChildren, type ReactNode, useMemo } from 'react' import { useWalletCapabilitiesStateEffect } from 'state/walletCapabilities/hooks/useWalletCapabilitiesStateEffect' import { type Register, WagmiProvider } from 'wagmi' -export function createWeb3Provider(params: { - wagmiConfig: Register['config'] - reconnectOnMount?: boolean - includeCapabilitiesEffects?: boolean -}) { - const { wagmiConfig, reconnectOnMount = true, includeCapabilitiesEffects = true } = params - - const WalletCapabilitiesEffects: React.FC = () => { - useWalletCapabilitiesStateEffect() - return null - } +export function createWeb3Provider(params: { wagmiConfig: Register['config']; reconnectOnMount?: boolean }) { + const { wagmiConfig, reconnectOnMount = true } = params const Provider = ({ children }: { children: ReactNode }) => ( - {includeCapabilitiesEffects && } {children} @@ -42,3 +32,8 @@ function SolanaProvider({ children }: PropsWithChildren) { ) } + +export function WalletCapabilitiesEffects() { + useWalletCapabilitiesStateEffect() + return null +} diff --git a/apps/web/src/components/Web3Provider/walletConnect.ts b/apps/web/src/components/Web3Provider/walletConnect.ts index 65efebcd344..833664f5325 100644 --- a/apps/web/src/components/Web3Provider/walletConnect.ts +++ b/apps/web/src/components/Web3Provider/walletConnect.ts @@ -42,7 +42,7 @@ export const WC_PARAMS = { qrModalOptions: { themeVariables: { '--wcm-font-family': '"Inter custom", sans-serif', - '--wcm-z-index': Z_INDEX.modal.toString(), + '--wcm-z-index': Z_INDEX.overlay.toString(), }, }, } diff --git a/apps/web/src/components/Web3Status/index.tsx b/apps/web/src/components/Web3Status/index.tsx index d9e7e06b8c9..3a87f8bd76c 100644 --- a/apps/web/src/components/Web3Status/index.tsx +++ b/apps/web/src/components/Web3Status/index.tsx @@ -10,7 +10,7 @@ import { useAccountIdentifier } from 'components/Web3Status/useAccountIdentifier import { useShowPendingAfterDelay } from 'components/Web3Status/useShowPendingAfterDelay' import { useModalState } from 'hooks/useModalState' import { atom, useAtom } from 'jotai' -import styled from 'lib/styled-components' +import { deprecatedStyled } from 'lib/styled-components' import { forwardRef, RefObject, useCallback, useEffect, useRef } from 'react' import { Trans, useTranslation } from 'react-i18next' import { AnimatePresence, Button, ButtonProps, Flex, Popover, Text } from 'ui/src' @@ -23,7 +23,7 @@ import Trace from 'uniswap/src/features/telemetry/Trace' import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { isIFramed } from 'utils/isIFramed' -const TextStyled = styled.span<{ marginRight?: number }>` +const TextStyled = deprecatedStyled.span<{ marginRight?: number }>` flex: 1 1 auto; text-overflow: ellipsis; white-space: nowrap; @@ -55,7 +55,7 @@ const Web3StatusGeneric = forwardRef(function Web3S ) }) -const AddressAndChevronContainer = styled.div<{ $loading?: boolean }>` +const AddressAndChevronContainer = deprecatedStyled.div<{ $loading?: boolean }>` display: flex; opacity: ${({ $loading, theme }) => $loading && theme.opacity.disabled}; align-items: center; @@ -90,7 +90,7 @@ const ExistingUserCTAButton = forwardRef void } ) }) -export const Web3StatusRef = atom | undefined>(undefined) +export const Web3StatusRef = atom | undefined>(undefined) function Web3StatusInner() { const activeAddresses = useActiveAddresses() @@ -109,9 +109,12 @@ function Web3StatusInner() { accountDrawer.toggle() }, [accountDrawer]) - const { hasPendingActivity, pendingActivityCount, isOnlyUnichainPendingActivity } = usePendingActivity() + const { hasPendingActivity, pendingActivityCount, hasL1PendingActivity } = usePendingActivity() const { accountIdentifier, hasUnitag } = useAccountIdentifier() - const showLoadingState = useShowPendingAfterDelay(hasPendingActivity, isOnlyUnichainPendingActivity) + const showLoadingState = useShowPendingAfterDelay({ + hasPendingActivity, + hasL1PendingActivity, + }) // TODO(WEB-4173): Remove isIFrame check when we can update wagmi to version >= 2.9.4 if (isConnecting && !isIFramed()) { diff --git a/apps/web/src/components/Web3Status/useShowPendingAfterDelay.ts b/apps/web/src/components/Web3Status/useShowPendingAfterDelay.ts index 8369473b86d..6684e011061 100644 --- a/apps/web/src/components/Web3Status/useShowPendingAfterDelay.ts +++ b/apps/web/src/components/Web3Status/useShowPendingAfterDelay.ts @@ -1,30 +1,41 @@ -import { L2_TXN_DISMISS_MS } from 'constants/misc' -import { useCallback } from 'react' +import { DEFAULT_TXN_DISMISS_MS, L2_TXN_DISMISS_MS } from 'constants/misc' +import { useCallback, useMemo } from 'react' import { useBooleanState } from 'utilities/src/react/useBooleanState' import { useTimeout } from 'utilities/src/time/timing' -export function useShowPendingAfterDelay(hasPendingActivity: boolean, isOnlyUnichainPendingActivity: boolean): boolean { +interface UseShowPendingAfterDelayParams { + hasPendingActivity: boolean + hasL1PendingActivity: boolean +} + +export function useShowPendingAfterDelay({ + hasPendingActivity, + hasL1PendingActivity, +}: UseShowPendingAfterDelayParams): boolean { const { - value: showUnichainTxAnyways, - setTrue: setShowUnichainTxAnyways, - setFalse: resetShowUnichainTxAnyways, + value: showPendingTxAnyways, + setTrue: setShowPendingTxAnyways, + setFalse: resetShowPendingTxAnyways, } = useBooleanState(false) - // needs to rerender once `isOnlyUnichainPendingActivity` is true so useTimeout starts - const showUnichainTxAfterDelay = useCallback(() => { - if (isOnlyUnichainPendingActivity && hasPendingActivity) { - setShowUnichainTxAnyways() + // use longer delay for L1 transactions + const dismissDelay = useMemo( + () => (hasL1PendingActivity ? DEFAULT_TXN_DISMISS_MS : L2_TXN_DISMISS_MS), + [hasL1PendingActivity], + ) + + const showPendingTxAfterDelay = useCallback(() => { + if (hasPendingActivity) { + setShowPendingTxAnyways() return } - resetShowUnichainTxAnyways() - }, [isOnlyUnichainPendingActivity, setShowUnichainTxAnyways, resetShowUnichainTxAnyways, hasPendingActivity]) + resetShowPendingTxAnyways() + }, [setShowPendingTxAnyways, resetShowPendingTxAnyways, hasPendingActivity]) - useTimeout(showUnichainTxAfterDelay, L2_TXN_DISMISS_MS) + useTimeout(showPendingTxAfterDelay, dismissDelay) - const showLoadingState = isOnlyUnichainPendingActivity - ? hasPendingActivity && showUnichainTxAnyways - : hasPendingActivity + const showLoadingState = hasPendingActivity && showPendingTxAnyways return showLoadingState } diff --git a/apps/web/src/components/animations/Wiggle.tsx b/apps/web/src/components/animations/Wiggle.tsx index a250c0b1b0d..4d14f81d594 100644 --- a/apps/web/src/components/animations/Wiggle.tsx +++ b/apps/web/src/components/animations/Wiggle.tsx @@ -1,52 +1,59 @@ -import { forwardRef, PropsWithChildren, useState } from 'react' +import { forwardRef, PropsWithChildren } from 'react' import { Flex, FlexProps, useSporeColors } from 'ui/src' +import { useBooleanState } from 'utilities/src/react/useBooleanState' -const wiggleKeyframe = ` +const getWiggleKeyframe = ({ wiggleAmount = 20 }: { wiggleAmount?: number }) => { + return ` @keyframes wiggle { 0% { transform: rotate(0deg) scale(1); } 30% { - transform: rotate(20deg) scale(1.1); + transform: rotate(${wiggleAmount}deg) scale(1.05); } 60% { - transform: rotate(-10deg) scale(1.2); + transform: rotate(-${wiggleAmount / 2}deg) scale(1.1); } 100% { - transform: rotate(0deg) scale(1.15); + transform: rotate(0deg) scale(1.06); } } ` +} -export const Wiggle = forwardRef & { iconColor?: string }>( - ({ iconColor, children, ...props }, ref) => { - const [isHovering, setIsHovering] = useState(false) - const colors = useSporeColors() +export const Wiggle = forwardRef< + any, + PropsWithChildren & { wiggleAmount?: number; iconColor?: string; isAnimating?: boolean } +>(({ wiggleAmount = 20, iconColor, children, isAnimating, ...props }, ref) => { + const { value: isHovering, setTrue: setIsHovering, setFalse: setIsHoveringFalse } = useBooleanState(false) + const colors = useSporeColors() + const wiggleKeyframe = getWiggleKeyframe({ wiggleAmount }) + // Use external isAnimating prop if provided, otherwise use internal hover state + const shouldAnimate = isAnimating !== undefined ? isAnimating : isHovering - return ( - <> - - setIsHovering(true)} - onHoverOut={() => setIsHovering(false)} - {...props} - style={{ - animationName: isHovering ? 'wiggle' : 'none', - animationDuration: '0.5s', - animationTimingFunction: 'ease-in-out', - animationFillMode: 'forwards', - animationIterationCount: 1, - animationDirection: 'normal', - transition: 'fill 0.3s ease-in-out', - fill: isHovering ? iconColor || colors.neutral1.val : colors.neutral1.val, - }} - > - {children} - - - ) - }, -) + return ( + <> + + + {children} + + + ) +}) Wiggle.displayName = 'Wiggle' diff --git a/apps/web/src/components/delegation/DelegationMismatchModal.tsx b/apps/web/src/components/delegation/DelegationMismatchModal.tsx index f1145037613..d7cb7800186 100644 --- a/apps/web/src/components/delegation/DelegationMismatchModal.tsx +++ b/apps/web/src/components/delegation/DelegationMismatchModal.tsx @@ -3,14 +3,14 @@ import { WalletAlertBadge } from 'components/Badge/WalletAlertBadge' import { useWalletDisplay } from 'components/Web3Status/RecentlyConnectedModal' import { useAccount } from 'hooks/useAccount' import { useDisconnect } from 'hooks/useDisconnect' -import { useTheme } from 'lib/styled-components' +import { useMemo } from 'react' import { useTranslation } from 'react-i18next' -import { Flex, Text } from 'ui/src' +import { Flex, Text, useSporeColors } from 'ui/src' import { Blocked } from 'ui/src/components/icons/Blocked' import { Dialog } from 'uniswap/src/components/dialog/Dialog' import { uniswapUrls } from 'uniswap/src/constants/urls' import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants' -import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send.web' +import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { Trace } from 'uniswap/src/features/telemetry/Trace' import { useEvent } from 'utilities/src/react/hooks' @@ -23,7 +23,7 @@ function DelegationMismatchModal({ onClose }: DelegationMismatchModalProps) { const account = useAccount() const { displayName } = useWalletDisplay(account.address) const disconnect = useDisconnect() - const theme = useTheme() + const colors = useSporeColors() const walletName = account.connector?.name ?? t('common.your.connected.wallet') const iconSrc = account.connector?.icon @@ -32,7 +32,7 @@ function DelegationMismatchModal({ onClose }: DelegationMismatchModalProps) { t('smartWallets.delegationMismatchModal.features.1ClickSwaps'), <> {t('smartWallets.delegationMismatchModal.features.gasFreeSwaps')} - {` (${t('uniswapx.label')})`} + {` (${t('uniswapx.label')})`} , t('smartWallets.delegationMismatchModal.features.limitOrders'), ] @@ -62,6 +62,26 @@ function DelegationMismatchModal({ onClose }: DelegationMismatchModalProps) { handleTrackModalDismissed() }) + const primaryButton = useMemo( + () => ({ + text: t('common.button.disconnect'), + onPress: handleSwitchWallets, + variant: 'default' as const, + emphasis: 'secondary' as const, + }), + [t, handleSwitchWallets], + ) + + const secondaryButton = useMemo( + () => ({ + text: t('common.button.continue'), + onPress: handleContinue, + variant: 'default' as const, + emphasis: 'primary' as const, + }), + [t, handleContinue], + ) + return ( } icon={} - primaryButtonText={t('common.button.disconnect')} - primaryButtonOnClick={handleSwitchWallets} - primaryButtonVariant="default" - primaryButtonEmphasis="secondary" - secondaryButtonText={t('common.button.continue')} - secondaryButtonOnClick={handleContinue} - secondaryButtonVariant="default" - secondaryButtonEmphasis="primary" + primaryButton={primaryButton} + secondaryButton={secondaryButton} learnMoreUrl={uniswapUrls.helpArticleUrls.mismatchedImports} learnMoreTextColor="$accent1" learnMoreTextVariant="buttonLabel3" onClose={onClose} - buttonContainerProps={{ flexDirection: 'row', gap: '$spacing12' }} textAlign="left" > - + {FEATURES.map((feature, index) => ( - - + + {feature} diff --git a/apps/web/src/components/deprecated/Column.tsx b/apps/web/src/components/deprecated/Column.tsx index a2ef916e601..f5a5b43442f 100644 --- a/apps/web/src/components/deprecated/Column.tsx +++ b/apps/web/src/components/deprecated/Column.tsx @@ -1,8 +1,8 @@ -import styled from 'lib/styled-components' +import { deprecatedStyled } from 'lib/styled-components' import { Gap } from 'theme' /** @deprecated Please use `Flex` from `ui/src` going forward */ -const Column = styled.div<{ +const Column = deprecatedStyled.div<{ gap?: Gap | string flex?: string }>` @@ -14,12 +14,12 @@ const Column = styled.div<{ ` /** @deprecated Please use `Flex` from `ui/src` going forward */ -export const ColumnCenter = styled(Column)` +export const ColumnCenter = deprecatedStyled(Column)` width: 100%; align-items: center; ` -export const AutoColumn = styled.div<{ +export const AutoColumn = deprecatedStyled.div<{ gap?: Gap | string justify?: 'stretch' | 'center' | 'start' | 'end' | 'flex-start' | 'flex-end' | 'space-between' grow?: true diff --git a/apps/web/src/components/deprecated/Row.tsx b/apps/web/src/components/deprecated/Row.tsx index 033ff87290a..8c66ee2eee0 100644 --- a/apps/web/src/components/deprecated/Row.tsx +++ b/apps/web/src/components/deprecated/Row.tsx @@ -1,4 +1,4 @@ -import styled from 'lib/styled-components' +import { deprecatedStyled } from 'lib/styled-components' import { Box } from 'rebass/styled-components' import { Gap } from 'theme' @@ -7,7 +7,7 @@ import { Gap } from 'theme' // Same applies to `RowFixed` and its negative margins. This component needs to be // further investigated and improved to make UI work easier. /** @deprecated Please use `Flex` from `ui/src` going forward */ -const Row = styled(Box)<{ +const Row = deprecatedStyled(Box)<{ width?: string align?: string justify?: string @@ -28,12 +28,12 @@ const Row = styled(Box)<{ ` /** @deprecated Please use `Flex` from `ui/src` going forward */ -export const RowBetween = styled(Row)` +export const RowBetween = deprecatedStyled(Row)` justify-content: space-between; ` /** @deprecated Please use `Flex` from `ui/src` going forward */ -export const AutoRow = styled(Row)<{ gap?: string; justify?: string }>` +export const AutoRow = deprecatedStyled(Row)<{ gap?: string; justify?: string }>` flex-wrap: wrap; margin: ${({ gap }) => gap && `-${gap}`}; justify-content: ${({ justify }) => justify && justify}; @@ -44,7 +44,7 @@ export const AutoRow = styled(Row)<{ gap?: string; justify?: string }>` ` /** @deprecated Please use `Flex` from `ui/src` going forward */ -export const RowFixed = styled(Row)<{ gap?: string; justify?: string }>` +export const RowFixed = deprecatedStyled(Row)<{ gap?: string; justify?: string }>` position: relative; width: fit-content; margin: ${({ gap }) => gap && `-${gap}`}; diff --git a/apps/web/src/components/earn/styled.tsx b/apps/web/src/components/earn/styled.tsx index e9b3d4909b7..945f2c4d61e 100644 --- a/apps/web/src/components/earn/styled.tsx +++ b/apps/web/src/components/earn/styled.tsx @@ -2,9 +2,9 @@ import uImage from 'assets/images/big_unicorn.png' import noise from 'assets/images/noise.png' import xlUnicorn from 'assets/images/xl_uni.png' import { AutoColumn } from 'components/deprecated/Column' -import styled from 'lib/styled-components' +import { deprecatedStyled } from 'lib/styled-components' -export const CardBGImage = styled.span<{ desaturate?: boolean }>` +export const CardBGImage = deprecatedStyled.span<{ desaturate?: boolean }>` background: url(${uImage}); width: 1000px; height: 600px; @@ -18,7 +18,7 @@ export const CardBGImage = styled.span<{ desaturate?: boolean }>` ${({ desaturate }) => desaturate && `filter: saturate(0)`} ` -export const CardBGImageSmaller = styled.span<{ desaturate?: boolean }>` +export const CardBGImageSmaller = deprecatedStyled.span<{ desaturate?: boolean }>` background: url(${xlUnicorn}); width: 1200px; height: 1200px; @@ -32,7 +32,7 @@ export const CardBGImageSmaller = styled.span<{ desaturate?: boolean }>` ${({ desaturate }) => desaturate && `filter: saturate(0)`} ` -export const CardNoise = styled.span` +export const CardNoise = deprecatedStyled.span` background: url(${noise}); background-size: cover; mix-blend-mode: overlay; @@ -46,13 +46,13 @@ export const CardNoise = styled.span` user-select: none; ` -export const CardSection = styled(AutoColumn)<{ disabled?: boolean }>` +export const CardSection = deprecatedStyled(AutoColumn)<{ disabled?: boolean }>` padding: 1rem; z-index: 1; opacity: ${({ disabled }) => disabled && '0.4'}; ` -export const Break = styled.div` +export const Break = deprecatedStyled.div` width: 100%; background-color: rgba(255, 255, 255, 0.2); height: 1px; diff --git a/apps/web/src/components/emptyWallet/EmptyWalletCards.tsx b/apps/web/src/components/emptyWallet/EmptyWalletCards.tsx index 0cd075a5b40..26ccd4e64ca 100644 --- a/apps/web/src/components/emptyWallet/EmptyWalletCards.tsx +++ b/apps/web/src/components/emptyWallet/EmptyWalletCards.tsx @@ -14,6 +14,7 @@ import type { FORServiceProvider } from 'uniswap/src/features/fiatOnRamp/types' import { useCexTransferProviders } from 'uniswap/src/features/fiatOnRamp/useCexTransferProviders' import { getServiceProviderLogo } from 'uniswap/src/features/fiatOnRamp/utils' import { ElementName } from 'uniswap/src/features/telemetry/constants' +import { isExtensionApp } from 'utilities/src/platform' import { useEvent } from 'utilities/src/react/hooks' const ICON_SIZE = 28 @@ -59,16 +60,19 @@ function CEXTransferLogo({ providers }: { providers: FORServiceProvider[] }) { export const EmptyWalletCards = ( { horizontalLayout, + growFullWidth, buyElementName, receiveElementName, cexTransferElementName, }: { horizontalLayout?: boolean + growFullWidth?: boolean buyElementName: ElementName receiveElementName: ElementName cexTransferElementName: ElementName } = { horizontalLayout: false, + growFullWidth: false, buyElementName: ElementName.EmptyStateBuy, receiveElementName: ElementName.EmptyStateReceive, cexTransferElementName: ElementName.EmptyStateCEXTransfer, @@ -83,7 +87,7 @@ export const EmptyWalletCards = ( const handleBuyCryptoClick = useEvent(() => { accountDrawer.close() - navigate(`/buy`, { replace: true }) + navigate(`/buy`, isExtensionApp ? { replace: true } : undefined) }) const handleReceiveCryptoClick = useOpenReceiveCryptoModal({ @@ -133,22 +137,34 @@ export const EmptyWalletCards = ( ], ) + // Determine layout mode + const isScrollableLayout = horizontalLayout && !growFullWidth + const isFullWidthLayout = horizontalLayout && growFullWidth + const needsLeftOffset = isScrollableLayout && fullWidth < EMPTY_WALLET_CARD_WIDTH - APP_PADDING + + // Calculate outer container width + const outerContainerWidth = isFullWidthLayout ? '100%' : horizontalLayout ? fullWidth : '100%' + + // Calculate inner grid width + const innerGridWidth = isFullWidthLayout ? '100%' : horizontalLayout ? EMPTY_WALLET_CARD_WIDTH : '100%' + + // Scroll styles for scrollable layout + const scrollStyles = isScrollableLayout + ? { + overflowX: 'scroll' as const, + scrollbarWidth: 'none' as const, + paddingBottom: 6, + } + : undefined + return ( {options.map((option) => ( ))} - {horizontalLayout && } + {isScrollableLayout && } ) diff --git a/apps/web/src/components/swap/DetailLineItem.tsx b/apps/web/src/components/swap/DetailLineItem.tsx index d5639bde48f..a906893b4d4 100644 --- a/apps/web/src/components/swap/DetailLineItem.tsx +++ b/apps/web/src/components/swap/DetailLineItem.tsx @@ -2,7 +2,7 @@ import { LoadingRow } from 'components/Loader/styled' import { MouseoverTooltip, TooltipSize } from 'components/Tooltip' import { useIsMobile } from 'hooks/screenSize/useIsMobile' import useHoverProps from 'hooks/useHoverProps' -import styled from 'lib/styled-components' +import { deprecatedStyled } from 'lib/styled-components' import { PropsWithChildren } from 'react' import { ThemedText } from 'theme/components' import { Flex } from 'ui/src' @@ -15,12 +15,12 @@ export type LineItemData = { loaderWidth?: number } -const LabelText = styled(ThemedText.BodySmall)<{ hasTooltip?: boolean }>` +const LabelText = deprecatedStyled(ThemedText.BodySmall)<{ hasTooltip?: boolean }>` cursor: ${({ hasTooltip }) => (hasTooltip ? 'help' : 'auto')}; color: ${({ theme }) => theme.neutral2}; ` -const DetailRowValue = styled(ThemedText.BodySmall)` +const DetailRowValue = deprecatedStyled(ThemedText.BodySmall)` text-align: right; overflow-wrap: break-word; ` diff --git a/apps/web/src/components/swap/GasBreakdownTooltip.tsx b/apps/web/src/components/swap/GasBreakdownTooltip.tsx index 2bc089cd568..a671cd0308d 100644 --- a/apps/web/src/components/swap/GasBreakdownTooltip.tsx +++ b/apps/web/src/components/swap/GasBreakdownTooltip.tsx @@ -2,7 +2,7 @@ import { Currency } from '@uniswap/sdk-core' import { AutoColumn } from 'components/deprecated/Column' import Row from 'components/deprecated/Row' import UniswapXRouterLabel, { UniswapXGradient } from 'components/RouterLabel/UniswapXRouterLabel' -import styled from 'lib/styled-components' +import { deprecatedStyled } from 'lib/styled-components' import { ReactNode } from 'react' import { Trans } from 'react-i18next' import { InterfaceTrade } from 'state/routing/types' @@ -18,7 +18,7 @@ import { getChainLabel } from 'uniswap/src/features/chains/utils' import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext' import { NumberType } from 'utilities/src/format/types' -const Container = styled(AutoColumn)` +const Container = deprecatedStyled(AutoColumn)` padding: 4px; ` @@ -102,7 +102,7 @@ function NetworkCostDescription({ native }: { native: Currency }) { ) } -const InlineUniswapXGradient = styled(UniswapXGradient)` +const InlineUniswapXGradient = deprecatedStyled(UniswapXGradient)` display: inline; ` export function UniswapXDescription() { diff --git a/apps/web/src/components/swap/GasEstimateTooltip.tsx b/apps/web/src/components/swap/GasEstimateTooltip.tsx index 30a240b3c2d..d9f5dffe282 100644 --- a/apps/web/src/components/swap/GasEstimateTooltip.tsx +++ b/apps/web/src/components/swap/GasEstimateTooltip.tsx @@ -4,7 +4,7 @@ import { LoadingOpacityContainer } from 'components/Loader/styled' import { UniswapXGradient, UniswapXRouterIcon } from 'components/RouterLabel/UniswapXRouterLabel' import { GasBreakdownTooltip } from 'components/swap/GasBreakdownTooltip' import { MouseoverTooltip, TooltipSize } from 'components/Tooltip' -import styled from 'lib/styled-components' +import { deprecatedStyled } from 'lib/styled-components' import { useMultichainContext } from 'state/multichain/useMultichainContext' import { SubmittableTrade } from 'state/routing/types' import { isUniswapXTrade } from 'state/routing/utils' @@ -14,7 +14,7 @@ import { ElementName, SwapEventName } from 'uniswap/src/features/telemetry/const import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { NumberType } from 'utilities/src/format/types' -const StyledGasIcon = styled(Gas)` +const StyledGasIcon = deprecatedStyled(Gas)` height: 16px; width: 16px; // We apply the following to all children of the SVG in order to override the default color diff --git a/apps/web/src/components/swap/SwapDetails.tsx b/apps/web/src/components/swap/SwapDetails.tsx index 825ed340d82..d8ccd2d0599 100644 --- a/apps/web/src/components/swap/SwapDetails.tsx +++ b/apps/web/src/components/swap/SwapDetails.tsx @@ -8,7 +8,7 @@ import SwapLineItem, { SwapLineItemType } from 'components/swap/SwapLineItem' import { SwapCallbackError, SwapShowAcceptChanges } from 'components/swap/styled' import { Allowance, AllowanceState } from 'hooks/usePermit2Allowance' import { SwapResult } from 'hooks/useSwapCallback' -import styled from 'lib/styled-components' +import { deprecatedStyled } from 'lib/styled-components' import { PropsWithChildren, ReactNode, useMemo } from 'react' import { Trans, useTranslation } from 'react-i18next' import { InterfaceTrade, LimitOrderTrade, RouterPreference } from 'state/routing/types' @@ -23,11 +23,11 @@ import Trace from 'uniswap/src/features/telemetry/Trace' import { useTrace } from 'utilities/src/telemetry/trace/TraceContext' import { formatSwapButtonClickEventProperties } from 'utils/loggingFormatters' -const DetailsContainer = styled(Column)` +const DetailsContainer = deprecatedStyled(Column)` padding: 0px 12px 8px; ` -const DropdownControllerWrapper = styled.div` +const DropdownControllerWrapper = deprecatedStyled.div` display: flex; align-items: center; margin-right: -6px; @@ -37,7 +37,7 @@ const DropdownControllerWrapper = styled.div` white-space: nowrap; ` -const DropdownButton = styled.button` +const DropdownButton = deprecatedStyled.button` padding: 0px 16px; margin-top: 4px; margin-bottom: 4px; @@ -50,7 +50,7 @@ const DropdownButton = styled.button` cursor: pointer; ` -const HelpLink = styled(ExternalLink)` +const HelpLink = deprecatedStyled(ExternalLink)` width: 100%; text-align: center; margin-top: 16px; diff --git a/apps/web/src/components/swap/SwapModalHeaderAmount.tsx b/apps/web/src/components/swap/SwapModalHeaderAmount.tsx index fa2f851f46e..b94f38ac8fe 100644 --- a/apps/web/src/components/swap/SwapModalHeaderAmount.tsx +++ b/apps/web/src/components/swap/SwapModalHeaderAmount.tsx @@ -3,7 +3,7 @@ import Column from 'components/deprecated/Column' import Row from 'components/deprecated/Row' import CurrencyLogo from 'components/Logo/CurrencyLogo' import { MouseoverTooltip } from 'components/Tooltip' -import styled from 'lib/styled-components' +import { deprecatedStyled } from 'lib/styled-components' import { PropsWithChildren, ReactNode } from 'react' import { TextProps } from 'rebass' import { ThemedText } from 'theme/components' @@ -13,7 +13,7 @@ import { useLocalizationContext } from 'uniswap/src/features/language/Localizati import { CurrencyField } from 'uniswap/src/types/currency' import { NumberType } from 'utilities/src/format/types' -const Label = styled(ThemedText.BodySmall)<{ cursor?: string }>` +const Label = deprecatedStyled(ThemedText.BodySmall)<{ cursor?: string }>` cursor: ${({ cursor }) => cursor}; color: ${({ theme }) => theme.neutral2}; margin-right: 8px; diff --git a/apps/web/src/components/swap/SwapPreview.tsx b/apps/web/src/components/swap/SwapPreview.tsx index 9b8b877c2dd..b376bd1277c 100644 --- a/apps/web/src/components/swap/SwapPreview.tsx +++ b/apps/web/src/components/swap/SwapPreview.tsx @@ -2,14 +2,14 @@ import { Currency, Percent, TradeType } from '@uniswap/sdk-core' import Column, { AutoColumn } from 'components/deprecated/Column' import { SwapModalHeaderAmount } from 'components/swap/SwapModalHeaderAmount' import { useUSDPrice } from 'hooks/useUSDPrice' -import styled from 'lib/styled-components' +import { deprecatedStyled } from 'lib/styled-components' import { Trans } from 'react-i18next' import { InterfaceTrade } from 'state/routing/types' import { isPreviewTrade } from 'state/routing/utils' import { ThemedText } from 'theme/components' import { CurrencyField } from 'uniswap/src/types/currency' -const HeaderContainer = styled(AutoColumn)` +const HeaderContainer = deprecatedStyled(AutoColumn)` margin-top: 0px; ` diff --git a/apps/web/src/components/swap/SwapSkeleton.tsx b/apps/web/src/components/swap/SwapSkeleton.tsx index 9ae639e5fbd..f45c0223827 100644 --- a/apps/web/src/components/swap/SwapSkeleton.tsx +++ b/apps/web/src/components/swap/SwapSkeleton.tsx @@ -1,18 +1,18 @@ import { ArrowContainer, ArrowWrapper } from 'components/swap/styled' -import styled, { useTheme } from 'lib/styled-components' +import { deprecatedStyled } from 'lib/styled-components' import { ArrowDown } from 'react-feather' import { Trans } from 'react-i18next' import { ThemedText } from 'theme/components' -import { styled as TamaguiStyled } from 'ui/src' +import { styled, useSporeColors } from 'ui/src' -const StyledArrowWrapper = TamaguiStyled(ArrowWrapper, { +const StyledArrowWrapper = styled(ArrowWrapper, { position: 'absolute', left: '50%', transform: 'translate(-50%, -50%)', margin: 0, }) -const LoadingWrapper = styled.div` +const LoadingWrapper = deprecatedStyled.div` display: flex; flex-direction: column; gap: 4px; @@ -24,29 +24,29 @@ const LoadingWrapper = styled.div` background-color: ${({ theme }) => theme.surface1}; ` -const Blob = styled.div<{ width?: number; radius?: number }>` +const Blob = deprecatedStyled.div<{ width?: number; radius?: number }>` background-color: ${({ theme }) => theme.surface2}; border-radius: ${({ radius }) => (radius ?? 4) + 'px'}; height: 56px; width: ${({ width }) => (width ? width + 'px' : '100%')}; ` -const ModuleBlob = styled(Blob)` +const ModuleBlob = deprecatedStyled(Blob)` background-color: ${({ theme }) => theme.surface3}; height: 36px; ` -const TitleColumn = styled.div` +const TitleColumn = deprecatedStyled.div` padding: 8px; ` -const Row = styled.div` +const Row = deprecatedStyled.div` display: flex; align-items: center; justify-content: space-between; ` -const InputColumn = styled.div` +const InputColumn = deprecatedStyled.div` display: flex; flex-flow: column; background-color: ${({ theme }) => theme.surface2}; @@ -56,7 +56,7 @@ const InputColumn = styled.div` padding: 48px 12px; ` -const OutputWrapper = styled.div` +const OutputWrapper = deprecatedStyled.div` position: relative; ` @@ -84,7 +84,7 @@ function FloatingButton() { } export function SwapSkeleton() { - const theme = useTheme() + const colors = useSporeColors() return ( @@ -95,7 +95,7 @@ export function SwapSkeleton() { - + diff --git a/apps/web/src/components/swap/TradePrice.tsx b/apps/web/src/components/swap/TradePrice.tsx index f6474696e36..74dd6a245ab 100644 --- a/apps/web/src/components/swap/TradePrice.tsx +++ b/apps/web/src/components/swap/TradePrice.tsx @@ -1,6 +1,6 @@ import { Currency, Price } from '@uniswap/sdk-core' import { useUSDPrice } from 'hooks/useUSDPrice' -import styled from 'lib/styled-components' +import { deprecatedStyled } from 'lib/styled-components' import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount' import { useCallback, useMemo, useState } from 'react' import { ThemedText } from 'theme/components' @@ -11,7 +11,7 @@ interface TradePriceProps { price: Price } -const StyledPriceContainer = styled.button` +const StyledPriceContainer = deprecatedStyled.button` background-color: transparent; border: none; cursor: pointer; diff --git a/apps/web/src/components/swap/__snapshots__/SwapDetails.test.tsx.snap b/apps/web/src/components/swap/__snapshots__/SwapDetails.test.tsx.snap index d01fecec184..d9a1e29e215 100644 --- a/apps/web/src/components/swap/__snapshots__/SwapDetails.test.tsx.snap +++ b/apps/web/src/components/swap/__snapshots__/SwapDetails.test.tsx.snap @@ -3,16 +3,8 @@ exports[`SwapDetails.tsx > renders a preview trade while disabling submission 1`] = ` .c0 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; flex-direction: column; - -webkit-box-pack: start; - -webkit-justify-content: flex-start; - -ms-flex-pack: start; justify-content: flex-start; gap: 8px; } @@ -25,28 +17,17 @@ exports[`SwapDetails.tsx > renders a preview trade while disabling submission 1` .c3 { width: 100%; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; display: flex; padding: 0; - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; align-items: center; - -webkit-box-pack: start; - -webkit-justify-content: flex-start; - -ms-flex-pack: start; justify-content: flex-start; } .c4 { - -webkit-flex-wrap: wrap; - -ms-flex-wrap: wrap; flex-wrap: wrap; } -.c4 > * { +.c4>* { margin: !important; } @@ -60,16 +41,19 @@ exports[`SwapDetails.tsx > renders a preview trade while disabling submission 1` >
- + +
- +
+
+
+
+ + + +
+
+ + New address + + + You haven’t transacted with this address before. Make sure it’s the correct address before continuing. + +
+
+ + 0x9984b4b4E408e8D618A879e5315BD30952c89103 + +
+
+
+ +
- + +
diff --git a/apps/web/src/pages/Swap/Send/__snapshots__/SendCurrencyInputForm.test.tsx.snap b/apps/web/src/pages/Swap/Send/__snapshots__/SendCurrencyInputForm.test.tsx.snap index 89853f50386..28155e60177 100644 --- a/apps/web/src/pages/Swap/Send/__snapshots__/SendCurrencyInputForm.test.tsx.snap +++ b/apps/web/src/pages/Swap/Send/__snapshots__/SendCurrencyInputForm.test.tsx.snap @@ -9,8 +9,6 @@ exports[`SendCurrencyInputform > renders input in fiat correctly 1`] = ` font-weight: 485; outline: none; border: none; - -webkit-flex: 1 1 auto; - -ms-flex: 1 1 auto; flex: 1 1 auto; background-color: transparent; font-size: 28px; @@ -22,7 +20,7 @@ exports[`SendCurrencyInputform > renders input in fiat correctly 1`] = ` text-align: right; } -.c1::-webkit-search-decoration { +.c1 ::-webkit-search-decoration { -webkit-appearance: none; } @@ -30,25 +28,13 @@ exports[`SendCurrencyInputform > renders input in fiat correctly 1`] = ` -moz-appearance: textfield; } -.c1::-webkit-outer-spin-button, -.c1::-webkit-inner-spin-button { +.c1 ::-webkit-outer-spin-button, +.c1 ::-webkit-inner-spin-button { -webkit-appearance: none; } -.c1::-webkit-input-placeholder { - color: rgba(19,19,19,0.35); -} - -.c1::-moz-placeholder { - color: rgba(19,19,19,0.35); -} - -.c1:-ms-input-placeholder { - color: rgba(19,19,19,0.35); -} - -.c1::placeholder { - color: rgba(19,19,19,0.35); +.c1 ::placeholder { + color: rgba(19, 19, 19, 0.35); } .c2 { @@ -62,19 +48,7 @@ exports[`SendCurrencyInputform > renders input in fiat correctly 1`] = ` line-height: 60px; } -.c2::-webkit-input-placeholder { - opacity: 1; -} - -.c2::-moz-placeholder { - opacity: 1; -} - -.c2:-ms-input-placeholder { - opacity: 1; -} - -.c2::placeholder { +.c2 ::placeholder { opacity: 1; } @@ -90,9 +64,6 @@ exports[`SendCurrencyInputform > renders input in fiat correctly 1`] = ` } .c0 { - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; user-select: none; color: #131313; text-align: left; @@ -103,9 +74,6 @@ exports[`SendCurrencyInputform > renders input in fiat correctly 1`] = ` .c4 { color: #131313; - -webkit-letter-spacing: -0.01em; - -moz-letter-spacing: -0.01em; - -ms-letter-spacing: -0.01em; letter-spacing: -0.01em; } @@ -145,6 +113,7 @@ exports[`SendCurrencyInputform > renders input in fiat correctly 1`] = ` autocomplete="off" autocorrect="off" class="c1 c2" + data-testid="send-form-amount-input" inputmode="decimal" maxlength="79" minlength="1" @@ -254,7 +223,7 @@ exports[`SendCurrencyInputform > renders input in fiat correctly 1`] = ` class="_display-flex _alignItems-stretch _flexBasis-auto _boxSizing-border-box _position-relative _minHeight-0px _minWidth-0px _flexShrink-0 _flexDirection-row" >
renders input in fiat correctly 1`] = `
- -

- +
+ +
+ +
+
+
+
+
+
+
+ + Select a token + +
+
+ + + +
+
+
+
+
+
+ + + +
+
+ + + +
+
+
+
+
+
+
+
+ +
+
+
+ + + +
+
+
+
+
+
+
+
+ + + + + Only Mainnet tokens are available for limits. + +
+
+
+
+
+
+
+ + + +
+ + Your tokens + +
+
+
+
+
+
+
+ + No tokens yet + + + Buy crypto with a card or bank to send tokens. + +
+
+
+
+ + Buy crypto + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
renders input in token amount correctly 1`] = ` font-weight: 485; outline: none; border: none; - -webkit-flex: 1 1 auto; - -ms-flex: 1 1 auto; flex: 1 1 auto; background-color: transparent; font-size: 28px; @@ -313,7 +551,7 @@ exports[`SendCurrencyInputform > renders input in token amount correctly 1`] = ` text-align: right; } -.c0::-webkit-search-decoration { +.c0 ::-webkit-search-decoration { -webkit-appearance: none; } @@ -321,25 +559,13 @@ exports[`SendCurrencyInputform > renders input in token amount correctly 1`] = ` -moz-appearance: textfield; } -.c0::-webkit-outer-spin-button, -.c0::-webkit-inner-spin-button { +.c0 ::-webkit-outer-spin-button, +.c0 ::-webkit-inner-spin-button { -webkit-appearance: none; } -.c0::-webkit-input-placeholder { - color: rgba(19,19,19,0.35); -} - -.c0::-moz-placeholder { - color: rgba(19,19,19,0.35); -} - -.c0:-ms-input-placeholder { - color: rgba(19,19,19,0.35); -} - -.c0::placeholder { - color: rgba(19,19,19,0.35); +.c0 ::placeholder { + color: rgba(19, 19, 19, 0.35); } .c1 { @@ -353,19 +579,7 @@ exports[`SendCurrencyInputform > renders input in token amount correctly 1`] = ` line-height: 60px; } -.c1::-webkit-input-placeholder { - opacity: 1; -} - -.c1::-moz-placeholder { - opacity: 1; -} - -.c1:-ms-input-placeholder { - opacity: 1; -} - -.c1::placeholder { +.c1 ::placeholder { opacity: 1; } @@ -382,9 +596,6 @@ exports[`SendCurrencyInputform > renders input in token amount correctly 1`] = ` .c3 { color: #131313; - -webkit-letter-spacing: -0.01em; - -moz-letter-spacing: -0.01em; - -ms-letter-spacing: -0.01em; letter-spacing: -0.01em; } @@ -418,6 +629,7 @@ exports[`SendCurrencyInputform > renders input in token amount correctly 1`] = ` autocomplete="off" autocorrect="off" class="c0 c1" + data-testid="send-form-amount-input" inputmode="decimal" maxlength="79" minlength="1" @@ -527,7 +739,7 @@ exports[`SendCurrencyInputform > renders input in token amount correctly 1`] = ` class="_display-flex _alignItems-stretch _flexBasis-auto _boxSizing-border-box _position-relative _minHeight-0px _minWidth-0px _flexShrink-0 _flexDirection-row" >
renders input in token amount correctly 1`] = `
- -

- +
+ +
+ +
+
+
+
+
+
+
+ + Select a token + +
+
+ + + +
+
+
+
+
+
+ + + +
+
+ + + +
+
+
+
+
+
+
+
+ +
+
+
+ + + +
+
+
+
+
+
+
+
+ + + + + Only Mainnet tokens are available for limits. + +
+
+
+
+
+
+
+ + + +
+ + Your tokens + +
+
+
+
+
+
+
+ + No tokens yet + + + Buy crypto with a card or bank to send tokens. + +
+
+
+
+ + Buy crypto + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
should render placeholder values 1`] = ` font-weight: 485; outline: none; border: none; - -webkit-flex: 1 1 auto; - -ms-flex: 1 1 auto; flex: 1 1 auto; background-color: transparent; font-size: 28px; @@ -586,7 +1067,7 @@ exports[`SendCurrencyInputform > should render placeholder values 1`] = ` text-align: right; } -.c1::-webkit-search-decoration { +.c1 ::-webkit-search-decoration { -webkit-appearance: none; } @@ -594,25 +1075,13 @@ exports[`SendCurrencyInputform > should render placeholder values 1`] = ` -moz-appearance: textfield; } -.c1::-webkit-outer-spin-button, -.c1::-webkit-inner-spin-button { +.c1 ::-webkit-outer-spin-button, +.c1 ::-webkit-inner-spin-button { -webkit-appearance: none; } -.c1::-webkit-input-placeholder { - color: rgba(19,19,19,0.35); -} - -.c1::-moz-placeholder { - color: rgba(19,19,19,0.35); -} - -.c1:-ms-input-placeholder { - color: rgba(19,19,19,0.35); -} - -.c1::placeholder { - color: rgba(19,19,19,0.35); +.c1 ::placeholder { + color: rgba(19, 19, 19, 0.35); } .c2 { @@ -626,19 +1095,7 @@ exports[`SendCurrencyInputform > should render placeholder values 1`] = ` line-height: 60px; } -.c2::-webkit-input-placeholder { - opacity: 1; -} - -.c2::-moz-placeholder { - opacity: 1; -} - -.c2:-ms-input-placeholder { - opacity: 1; -} - -.c2::placeholder { +.c2 ::placeholder { opacity: 1; } @@ -654,23 +1111,17 @@ exports[`SendCurrencyInputform > should render placeholder values 1`] = ` } .c0 { - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; user-select: none; color: #131313; text-align: left; font-size: 70px; font-weight: 500; line-height: 60px; - color: rgba(19,19,19,0.35); + color: rgba(19, 19, 19, 0.35); } .c4 { color: #131313; - -webkit-letter-spacing: -0.01em; - -moz-letter-spacing: -0.01em; - -ms-letter-spacing: -0.01em; letter-spacing: -0.01em; } @@ -710,6 +1161,7 @@ exports[`SendCurrencyInputform > should render placeholder values 1`] = ` autocomplete="off" autocorrect="off" class="c1 c2" + data-testid="send-form-amount-input" inputmode="decimal" maxlength="79" minlength="1" @@ -817,7 +1269,7 @@ exports[`SendCurrencyInputform > should render placeholder values 1`] = ` class="_display-flex _alignItems-stretch _flexBasis-auto _boxSizing-border-box _position-relative _minHeight-0px _minWidth-0px _flexShrink-0 _flexDirection-row" >
should render placeholder values 1`] = `
- -

- +
+ +
+ +
+
+
+
+
+
+
+ + Select a token + +
+
+ + + +
+
+
+
+
+
+ + + +
+
+ + + +
+
+
+
+
+
+
+
+ +
+
+
+ + + +
+
+
+
+
+
+
+
+ + + + + Only Mainnet tokens are available for limits. + +
+
+
+
+
+
+
+ + + +
+ + Your tokens + +
+
+
+
+
+
+
+ + No tokens yet + + + Buy crypto with a card or bank to send tokens. + +
+
+
+
+ + Buy crypto + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
should render correctly with no verified recipi line-height: 24px; } -.c0::-webkit-input-placeholder { - color: rgba(19,19,19,0.35); -} - -.c0::-moz-placeholder { - color: rgba(19,19,19,0.35); -} - -.c0:-ms-input-placeholder { - color: rgba(19,19,19,0.35); -} - -.c0::placeholder { - color: rgba(19,19,19,0.35); +.c0 ::placeholder { + color: rgba(19, 19, 19, 0.35); } should render correctly with no verified recipi exports[`SendCurrencyInputform > should render correctly with unitag 1`] = ` .c5 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; flex-direction: column; - -webkit-box-pack: start; - -webkit-justify-content: flex-start; - -ms-flex-pack: start; justify-content: flex-start; } @@ -98,111 +78,71 @@ exports[`SendCurrencyInputform > should render correctly with unitag 1`] = ` .c1 { width: 100%; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; display: flex; padding: 0; - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; align-items: center; - -webkit-box-pack: start; - -webkit-justify-content: flex-start; - -ms-flex-pack: start; justify-content: flex-start; } .c3 { width: 100%; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; display: flex; padding: 0; - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; align-items: center; - -webkit-box-pack: start; - -webkit-justify-content: flex-start; - -ms-flex-pack: start; justify-content: flex-start; gap: 12px; } .c6 { width: 100%; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; display: flex; padding: 0; - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; align-items: center; - -webkit-box-pack: start; - -webkit-justify-content: flex-start; - -ms-flex-pack: start; justify-content: flex-start; gap: 4px; } .c7 { color: #131313; - -webkit-letter-spacing: -0.01em; - -moz-letter-spacing: -0.01em; - -ms-letter-spacing: -0.01em; letter-spacing: -0.01em; } .c8 { - color: rgba(19,19,19,0.63); - -webkit-letter-spacing: -0.01em; - -moz-letter-spacing: -0.01em; - -ms-letter-spacing: -0.01em; + color: rgba(19, 19, 19, 0.63); letter-spacing: -0.01em; } .c2 { padding: 6px 0px; - -webkit-box-pack: justify; - -webkit-justify-content: space-between; - -ms-flex-pack: justify; justify-content: space-between; } .c4 { - -webkit-text-decoration: none; text-decoration: none; cursor: pointer; - -webkit-transition-duration: 125ms; transition-duration: 125ms; } -.c4:hover { +.c4 :hover { opacity: 0.6; } -.c4:active { +.c4 :active { opacity: 0.4; } .c9 { - color: rgba(19,19,19,0.35); - -webkit-text-decoration: none; + color: rgba(19, 19, 19, 0.35); text-decoration: none; cursor: pointer; - -webkit-transition-duration: 125ms; transition-duration: 125ms; } -.c9:hover { +.c9 :hover { opacity: 0.6; } -.c9:active { +.c9 :active { opacity: 0.4; } @@ -228,13 +168,14 @@ exports[`SendCurrencyInputform > should render correctly with unitag 1`] = ` >
should render correctly with unitag 1`] = ` >
should render correctly with unitag 1`] = ` exports[`SendCurrencyInputform > should render correctly with verified recipient 1`] = ` .c5 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; flex-direction: column; - -webkit-box-pack: start; - -webkit-justify-content: flex-start; - -ms-flex-pack: start; justify-content: flex-start; } @@ -326,111 +260,71 @@ exports[`SendCurrencyInputform > should render correctly with verified recipient .c1 { width: 100%; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; display: flex; padding: 0; - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; align-items: center; - -webkit-box-pack: start; - -webkit-justify-content: flex-start; - -ms-flex-pack: start; justify-content: flex-start; } .c3 { width: 100%; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; display: flex; padding: 0; - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; align-items: center; - -webkit-box-pack: start; - -webkit-justify-content: flex-start; - -ms-flex-pack: start; justify-content: flex-start; gap: 12px; } .c6 { width: 100%; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; display: flex; padding: 0; - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; align-items: center; - -webkit-box-pack: start; - -webkit-justify-content: flex-start; - -ms-flex-pack: start; justify-content: flex-start; gap: 4px; } .c7 { color: #131313; - -webkit-letter-spacing: -0.01em; - -moz-letter-spacing: -0.01em; - -ms-letter-spacing: -0.01em; letter-spacing: -0.01em; } .c8 { - color: rgba(19,19,19,0.63); - -webkit-letter-spacing: -0.01em; - -moz-letter-spacing: -0.01em; - -ms-letter-spacing: -0.01em; + color: rgba(19, 19, 19, 0.63); letter-spacing: -0.01em; } .c2 { padding: 6px 0px; - -webkit-box-pack: justify; - -webkit-justify-content: space-between; - -ms-flex-pack: justify; justify-content: space-between; } .c4 { - -webkit-text-decoration: none; text-decoration: none; cursor: pointer; - -webkit-transition-duration: 125ms; transition-duration: 125ms; } -.c4:hover { +.c4 :hover { opacity: 0.6; } -.c4:active { +.c4 :active { opacity: 0.4; } .c9 { - color: rgba(19,19,19,0.35); - -webkit-text-decoration: none; + color: rgba(19, 19, 19, 0.35); text-decoration: none; cursor: pointer; - -webkit-transition-duration: 125ms; transition-duration: 125ms; } -.c9:hover { +.c9 :hover { opacity: 0.6; } -.c9:active { +.c9 :active { opacity: 0.4; } @@ -456,13 +350,14 @@ exports[`SendCurrencyInputform > should render correctly with verified recipient >
should render correctly with verified recipient >
should render placeholder values 1`] = ` line-height: 24px; } -.c0::-webkit-input-placeholder { - color: rgba(19,19,19,0.35); -} - -.c0::-moz-placeholder { - color: rgba(19,19,19,0.35); -} - -.c0:-ms-input-placeholder { - color: rgba(19,19,19,0.35); -} - -.c0::placeholder { - color: rgba(19,19,19,0.35); +.c0 ::placeholder { + color: rgba(19, 19, 19, 0.35); } should render input in fiat correctly 1`] = ` .c0 { color: #131313; - -webkit-letter-spacing: -0.01em; - -moz-letter-spacing: -0.01em; - -ms-letter-spacing: -0.01em; letter-spacing: -0.01em; } .c1 { - color: rgba(19,19,19,0.63); - -webkit-letter-spacing: -0.01em; - -moz-letter-spacing: -0.01em; - -ms-letter-spacing: -0.01em; + color: rgba(19, 19, 19, 0.63); letter-spacing: -0.01em; } @@ -111,7 +105,7 @@ exports[`SendReviewModal > should render input in fiat correctly 1`] = ` data-testid="account-icon" >
should render input in fiat correctly 1`] = ` class="_display-flex _alignItems-stretch _flexBasis-auto _boxSizing-border-box _position-relative _minHeight-0px _minWidth-0px _flexShrink-0 _flexDirection-row _alignSelf-stretch" > - -
- +
+
+
+
+ + + +
+
+ + Is this a wallet address? + + + You’re about to send tokens to a special type of address - a smart contract. Double-check it’s the address you intended to send to. If it’s wrong, your tokens could be lost forever. + +
+
+
+ +
- +
+
diff --git a/apps/web/src/pages/Swap/Swap.e2e.test.ts b/apps/web/src/pages/Swap/Swap.e2e.test.ts index e6fdc09d0d4..79b231e8d51 100644 --- a/apps/web/src/pages/Swap/Swap.e2e.test.ts +++ b/apps/web/src/pages/Swap/Swap.e2e.test.ts @@ -4,24 +4,34 @@ import { TestID } from 'uniswap/src/test/fixtures/testIDs' const test = getTest() -test.describe('Swap', () => { - test('should default inputs from URL params ', async ({ page }) => { - await page.goto(`/swap?inputCurrency=${USDT.address}`) - await expect(page.getByTestId(TestID.ChooseInputToken + '-label')).toHaveText('USDT') +test.describe( + 'Swap', + { + tag: '@team:apps-swap', + annotation: [ + { type: 'DD_TAGS[team]', description: 'apps-swap' }, + { type: 'DD_TAGS[test.type]', description: 'web-e2e' }, + ], + }, + () => { + test('should default inputs from URL params ', async ({ page }) => { + await page.goto(`/swap?inputCurrency=${USDT.address}`) + await expect(page.getByTestId(TestID.ChooseInputToken + '-label')).toHaveText('USDT') - await page.goto(`/swap?outputCurrency=${USDT.address}`) - await expect(page.getByTestId(TestID.ChooseOutputToken + '-label')).toHaveText('USDT') + await page.goto(`/swap?outputCurrency=${USDT.address}`) + await expect(page.getByTestId(TestID.ChooseOutputToken + '-label')).toHaveText('USDT') - await page.goto(`/swap?inputCurrency=ETH&outputCurrency=${USDT.address}`) - await expect(page.getByTestId(TestID.ChooseInputToken + '-label')).toHaveText('ETH') - await expect(page.getByTestId(TestID.ChooseOutputToken + '-label')).toHaveText('USDT') - }) + await page.goto(`/swap?inputCurrency=ETH&outputCurrency=${USDT.address}`) + await expect(page.getByTestId(TestID.ChooseInputToken + '-label')).toHaveText('ETH') + await expect(page.getByTestId(TestID.ChooseOutputToken + '-label')).toHaveText('USDT') + }) - test('should reset the dependent input when the independent input is cleared', async ({ page }) => { - await page.goto(`/swap?inputCurrency=ETH&outputCurrency=${USDT.address}`) - await page.getByTestId(TestID.AmountInputIn).fill('0.01') - await page.getByTestId(TestID.AmountInputIn).clear() - await expect(page.getByTestId(TestID.AmountInputIn)).toHaveValue('') - await expect(page.getByTestId(TestID.AmountInputOut)).toHaveValue('') - }) -}) + test('should reset the dependent input when the independent input is cleared', async ({ page }) => { + await page.goto(`/swap?inputCurrency=ETH&outputCurrency=${USDT.address}`) + await page.getByTestId(TestID.AmountInputIn).fill('0.01') + await page.getByTestId(TestID.AmountInputIn).clear() + await expect(page.getByTestId(TestID.AmountInputIn)).toHaveValue('') + await expect(page.getByTestId(TestID.AmountInputOut)).toHaveValue('') + }) + }, +) diff --git a/apps/web/src/pages/Swap/SwapSettings.e2e.test.ts b/apps/web/src/pages/Swap/SwapSettings.e2e.test.ts index d0ef8b2c207..cbb23a7e6b0 100644 --- a/apps/web/src/pages/Swap/SwapSettings.e2e.test.ts +++ b/apps/web/src/pages/Swap/SwapSettings.e2e.test.ts @@ -3,22 +3,32 @@ import { TestID } from 'uniswap/src/test/fixtures/testIDs' const test = getTest() -test.describe('Swap Settings', () => { - test('opens and closes the settings menu', async ({ page }) => { - await page.goto('/swap') - await page.getByTestId(TestID.SwapSettings).click() - await expect(page.getByText('Max slippage')).toBeVisible() - await expect(page.getByText('Default')).toBeVisible() - await page.getByTestId(TestID.SwapSettings).click() - await expect(page.getByText('Max slippage')).not.toBeVisible() - }) +test.describe( + 'Swap Settings', + { + tag: '@team:apps-swap', + annotation: [ + { type: 'DD_TAGS[team]', description: 'apps-swap' }, + { type: 'DD_TAGS[test.type]', description: 'web-e2e' }, + ], + }, + () => { + test('opens and closes the settings menu', async ({ page }) => { + await page.goto('/swap') + await page.getByTestId(TestID.SwapSettings).click() + await expect(page.getByText('Max slippage')).toBeVisible() + await expect(page.getByText('Default')).toBeVisible() + await page.getByTestId(TestID.SwapSettings).click() + await expect(page.getByText('Max slippage')).not.toBeVisible() + }) - test('should open the mobile settings menu', async ({ page }) => { - await page.setViewportSize({ width: 375, height: 667 }) - await page.goto('/swap') - await page.getByTestId(TestID.SwapSettings).click() - await expect(page.getByText('Max slippage')).toBeVisible() - await expect(page.getByText('Default')).toBeVisible() - await expect(page.getByTestId(TestID.MobileWebSettingsMenu).first()).toBeVisible() - }) -}) + test('should open the mobile settings menu', async ({ page }) => { + await page.setViewportSize({ width: 375, height: 667 }) + await page.goto('/swap') + await page.getByTestId(TestID.SwapSettings).click() + await expect(page.getByText('Max slippage')).toBeVisible() + await expect(page.getByText('Default')).toBeVisible() + await expect(page.getByTestId(TestID.MobileWebSettingsMenu).first()).toBeVisible() + }) + }, +) diff --git a/apps/web/src/pages/Swap/TokenSelector.e2e.test.ts b/apps/web/src/pages/Swap/TokenSelector.e2e.test.ts index 31f5d83ff4a..3c9a260ceab 100644 --- a/apps/web/src/pages/Swap/TokenSelector.e2e.test.ts +++ b/apps/web/src/pages/Swap/TokenSelector.e2e.test.ts @@ -4,54 +4,64 @@ import { TestID } from 'uniswap/src/test/fixtures/testIDs' const test = getTest() -test.describe('TokenSelector', () => { - test('output - should show bridging and top tokens sections if empty', async ({ page }) => { - await page.goto('/swap') - await page.getByTestId(TestID.ChooseOutputToken).click() - - await expect( - page.getByTestId(`${TestID.SectionHeaderPrefix}${OnchainItemSectionName.TrendingTokens}`), - ).toBeVisible() - await expect( - page.getByTestId(`${TestID.SectionHeaderPrefix}${OnchainItemSectionName.BridgingTokens}`), - ).toBeVisible() - }) - - test('output - should show top tokens sections if token selected', async ({ page }) => { - await page.goto('/swap') - await page.getByTestId(TestID.SwitchCurrenciesButton).click() - await page.getByTestId(TestID.ChooseOutputToken).click() - - await expect( - page.getByTestId(`${TestID.SectionHeaderPrefix}${OnchainItemSectionName.TrendingTokens}`), - ).toBeVisible() - await expect( - page.getByTestId(`${TestID.SectionHeaderPrefix}${OnchainItemSectionName.BridgingTokens}`), - ).not.toBeVisible() - }) - - test('input - should show top tokens sections if token selected', async ({ page }) => { - await page.goto('/swap') - await page.getByTestId(TestID.ChooseInputToken).click() - - await expect( - page.getByTestId(`${TestID.SectionHeaderPrefix}${OnchainItemSectionName.TrendingTokens}`), - ).toBeVisible() - await expect( - page.getByTestId(`${TestID.SectionHeaderPrefix}${OnchainItemSectionName.BridgingTokens}`), - ).not.toBeVisible() - }) - - test('input - should show bridging and top tokens sections if empty', async ({ page }) => { - await page.goto('/swap') - await page.getByTestId(TestID.SwitchCurrenciesButton).click() - await page.getByTestId(TestID.ChooseInputToken).click() - - await expect( - page.getByTestId(`${TestID.SectionHeaderPrefix}${OnchainItemSectionName.TrendingTokens}`), - ).toBeVisible() - await expect( - page.getByTestId(`${TestID.SectionHeaderPrefix}${OnchainItemSectionName.BridgingTokens}`), - ).toBeVisible() - }) -}) +test.describe( + 'TokenSelector', + { + tag: '@team:apps-swap', + annotation: [ + { type: 'DD_TAGS[team]', description: 'apps-swap' }, + { type: 'DD_TAGS[test.type]', description: 'web-e2e' }, + ], + }, + () => { + test('output - should show bridging and top tokens sections if empty', async ({ page }) => { + await page.goto('/swap') + await page.getByTestId(TestID.ChooseOutputToken).click() + + await expect( + page.getByTestId(`${TestID.SectionHeaderPrefix}${OnchainItemSectionName.TrendingTokens}`), + ).toBeVisible() + await expect( + page.getByTestId(`${TestID.SectionHeaderPrefix}${OnchainItemSectionName.BridgingTokens}`), + ).toBeVisible() + }) + + test('output - should show top tokens sections if token selected', async ({ page }) => { + await page.goto('/swap') + await page.getByTestId(TestID.SwitchCurrenciesButton).click() + await page.getByTestId(TestID.ChooseOutputToken).click() + + await expect( + page.getByTestId(`${TestID.SectionHeaderPrefix}${OnchainItemSectionName.TrendingTokens}`), + ).toBeVisible() + await expect( + page.getByTestId(`${TestID.SectionHeaderPrefix}${OnchainItemSectionName.BridgingTokens}`), + ).not.toBeVisible() + }) + + test('input - should show top tokens sections if token selected', async ({ page }) => { + await page.goto('/swap') + await page.getByTestId(TestID.ChooseInputToken).click() + + await expect( + page.getByTestId(`${TestID.SectionHeaderPrefix}${OnchainItemSectionName.TrendingTokens}`), + ).toBeVisible() + await expect( + page.getByTestId(`${TestID.SectionHeaderPrefix}${OnchainItemSectionName.BridgingTokens}`), + ).not.toBeVisible() + }) + + test('input - should show bridging and top tokens sections if empty', async ({ page }) => { + await page.goto('/swap') + await page.getByTestId(TestID.SwitchCurrenciesButton).click() + await page.getByTestId(TestID.ChooseInputToken).click() + + await expect( + page.getByTestId(`${TestID.SectionHeaderPrefix}${OnchainItemSectionName.TrendingTokens}`), + ).toBeVisible() + await expect( + page.getByTestId(`${TestID.SectionHeaderPrefix}${OnchainItemSectionName.BridgingTokens}`), + ).toBeVisible() + }) + }, +) diff --git a/apps/web/src/pages/Swap/UniswapX.anvil.e2e.test.ts b/apps/web/src/pages/Swap/UniswapX.anvil.e2e.test.ts index fd57b0cb00b..3c130fc737c 100644 --- a/apps/web/src/pages/Swap/UniswapX.anvil.e2e.test.ts +++ b/apps/web/src/pages/Swap/UniswapX.anvil.e2e.test.ts @@ -14,70 +14,80 @@ const test = getTest({ withAnvil: true }) const UNISWAP_X_ORDERS_ENDPOINT = `https://interface.gateway.uniswap.org/v2/orders?swapper=${TEST_WALLET_ADDRESS}&orderHashes=${ZERO_ADDRESS}` -test.describe('UniswapX', async () => { - test.beforeEach(async ({ page, anvil }) => { - await anvil.setErc20Balance({ - address: assume0xAddress(WETH9[UniverseChainId.Mainnet].address), - balance: parseEther('1000000'), - }) - await page.route(`${uniswapUrls.tradingApiUrl}${uniswapUrls.tradingApiPaths.quote}`, async (route, request) => { - const postData = await request.postData() - const data = JSON.parse(postData ?? '{}') - if (data.tokenOut === USDC_MAINNET.address) { - await route.continue() - } else { - await route.fulfill({ path: Mocks.UniswapX.quote }) - } - }) - await page.route(`${uniswapUrls.tradingApiUrl}${uniswapUrls.tradingApiPaths.order}`, async (route) => { - await route.fulfill({ path: Mocks.UniswapX.openOrder }) - }) - await page.goto(`/swap?inputCurrency=${WETH9[UniverseChainId.Mainnet].address}&outputCurrency=${DAI.address}`) +test.describe( + 'UniswapX', + { + tag: '@team:apps-swap', + annotation: [ + { type: 'DD_TAGS[team]', description: 'apps-swap' }, + { type: 'DD_TAGS[test.type]', description: 'web-e2e' }, + ], + }, + async () => { + test.beforeEach(async ({ page, anvil }) => { + await anvil.setErc20Balance({ + address: assume0xAddress(WETH9[UniverseChainId.Mainnet].address), + balance: parseEther('1000000'), + }) + await page.route(`${uniswapUrls.tradingApiUrl}${uniswapUrls.tradingApiPaths.quote}`, async (route, request) => { + const postData = await request.postData() + const data = JSON.parse(postData ?? '{}') + if (data.tokenOut === USDC_MAINNET.address) { + await route.continue() + } else { + await route.fulfill({ path: Mocks.UniswapX.quote }) + } + }) + await page.route(`${uniswapUrls.tradingApiUrl}${uniswapUrls.tradingApiPaths.order}`, async (route) => { + await route.fulfill({ path: Mocks.UniswapX.openOrder }) + }) + await page.goto(`/swap?inputCurrency=${WETH9[UniverseChainId.Mainnet].address}&outputCurrency=${DAI.address}`) - await page.getByTestId(TestID.AmountInputIn).fill('1') - await page.getByTestId(TestID.ReviewSwap).click() - await page.getByTestId(TestID.Swap).click() - }) + await page.getByTestId(TestID.AmountInputIn).fill('1') + await page.getByTestId(TestID.ReviewSwap).click() + await page.getByTestId(TestID.Swap).click() + }) - test('can swap using uniswapX with WETH as input', async ({ page }) => { - await page.route(UNISWAP_X_ORDERS_ENDPOINT, async (route) => { - await route.fulfill({ - path: Mocks.UniswapX.filledOrders, + test('can swap using uniswapX with WETH as input', async ({ page }) => { + await page.route(UNISWAP_X_ORDERS_ENDPOINT, async (route) => { + await route.fulfill({ + path: Mocks.UniswapX.filledOrders, + }) }) + + await expect(page.getByText('Approved')).toBeVisible() + await expect(page.getByRole('button', { name: 'Swapping 1.00 WETH for 3,665.13 DAI' })).toBeVisible() }) - await expect(page.getByText('Approved')).toBeVisible() - await expect(page.getByRole('button', { name: 'Swapping 1.00 WETH for 3,665.13 DAI' })).toBeVisible() - }) + test('renders error view if uniswapx order expires', async ({ page }) => { + await page.route(UNISWAP_X_ORDERS_ENDPOINT, async (route) => { + await route.fulfill({ path: Mocks.UniswapX.expiredOrders }) + }) - test('renders error view if uniswapx order expires', async ({ page }) => { - await page.route(UNISWAP_X_ORDERS_ENDPOINT, async (route) => { - await route.fulfill({ path: Mocks.UniswapX.expiredOrders }) + await expect(page.getByText('Swap expired')).toBeVisible() }) - await expect(page.getByText('Swap expired')).toBeVisible() - }) + test('cancels a pending uniswapx order', async ({ page }) => { + await page.getByTestId(TestID.Web3StatusConnected).click() + await page.getByText('Activity').click() + await page.getByText('Swapping').click() + await page.getByText('Cancel').click() + await page.getByRole('button', { name: 'Proceed' }).click() - test('cancels a pending uniswapx order', async ({ page }) => { - await page.getByTestId(TestID.Web3StatusConnected).click() - await page.getByText('Activity').click() - await page.getByText('Swapping').click() - await page.getByText('Cancel').click() - await page.getByRole('button', { name: 'Proceed' }).click() + await expect(page.getByText('Cancellation successful')).toBeVisible() + }) - await expect(page.getByText('Cancellation successful')).toBeVisible() - }) + test('deduplicates remote vs local uniswapx orders', async ({ page, graphql }) => { + await page.route(UNISWAP_X_ORDERS_ENDPOINT, async (route) => { + await route.fulfill({ path: Mocks.UniswapX.filledOrders }) + }) - test('deduplicates remote vs local uniswapx orders', async ({ page, graphql }) => { - await page.route(UNISWAP_X_ORDERS_ENDPOINT, async (route) => { - await route.fulfill({ path: Mocks.UniswapX.filledOrders }) + await page.getByTestId(TestID.Web3StatusConnected).click() + await page.getByText('Activity').click() + await graphql.intercept('ActivityWeb', Mocks.UniswapX.activity) + const activity = await page.getByTestId(TestID.ActivityContent) + await expect(activity.getByText('Swapping')).not.toBeVisible() + await expect(activity.getByText('Swapped')).toBeVisible() }) - - await page.getByTestId(TestID.Web3StatusConnected).click() - await page.getByText('Activity').click() - await graphql.intercept('ActivityWeb', Mocks.UniswapX.activity) - const activity = await page.getByTestId(TestID.ActivityContent) - await expect(activity.getByText('Swapping')).not.toBeVisible() - await expect(activity.getByText('Swapped')).toBeVisible() - }) -}) + }, +) diff --git a/apps/web/src/pages/Swap/Wrap.anvil.e2e.test.ts b/apps/web/src/pages/Swap/Wrap.anvil.e2e.test.ts index fbaf2fcf945..dba692f2dba 100644 --- a/apps/web/src/pages/Swap/Wrap.anvil.e2e.test.ts +++ b/apps/web/src/pages/Swap/Wrap.anvil.e2e.test.ts @@ -11,61 +11,71 @@ import { parseEther } from 'viem' const test = getTest({ withAnvil: true }) -test.describe('Wrap', () => { - test.describe.configure({ retries: 3 }) +test.describe( + 'Wrap', + { + tag: '@team:apps-swap', + annotation: [ + { type: 'DD_TAGS[team]', description: 'apps-swap' }, + { type: 'DD_TAGS[test.type]', description: 'web-e2e' }, + ], + }, + () => { + test.describe.configure({ retries: 3 }) - test('should unwrap WETH', async ({ page, anvil }) => { - const expectSingleTransaction = createExpectSingleTransaction({ - anvil, - address: TEST_WALLET_ADDRESS, - options: { blocks: 2 }, - }) + test('should unwrap WETH', async ({ page, anvil }) => { + const expectSingleTransaction = createExpectSingleTransaction({ + anvil, + address: TEST_WALLET_ADDRESS, + options: { blocks: 2 }, + }) - await stubTradingApiEndpoint({ page, endpoint: uniswapUrls.tradingApiPaths.swap }) + await stubTradingApiEndpoint({ page, endpoint: uniswapUrls.tradingApiPaths.swap }) - await anvil.setErc20Balance({ - address: assume0xAddress(WETH_ADDRESS(UniverseChainId.Mainnet)), - balance: parseEther('1'), - }) - await page.goto(`/swap`) + await anvil.setErc20Balance({ + address: assume0xAddress(WETH_ADDRESS(UniverseChainId.Mainnet)), + balance: parseEther('1'), + }) + await page.goto(`/swap`) - await page.getByTestId(TestID.ChooseInputToken).click() - // eslint-disable-next-line - await page.getByTestId('token-option-1-WETH').first().click() + await page.getByTestId(TestID.ChooseInputToken).click() + // eslint-disable-next-line + await page.getByTestId('token-option-1-WETH').first().click() - await page.getByTestId(TestID.ChooseOutputToken).click() - // eslint-disable-next-line - await page.getByTestId('token-option-1-ETH').first().click() + await page.getByTestId(TestID.ChooseOutputToken).click() + // eslint-disable-next-line + await page.getByTestId('token-option-1-ETH').first().click() - await page.getByTestId(TestID.AmountInputIn).fill('0.01') + await page.getByTestId(TestID.AmountInputIn).fill('0.01') - await expectSingleTransaction(async () => { - await page.getByTestId(TestID.ReviewSwap).click() - await expect(page.getByText('Unwrapped')).toBeVisible() - await expect(page.getByText('0.010 WETH for 0.010 ETH')).toBeVisible() - }) - }) - test('should wrap ETH', async ({ page, anvil }) => { - const expectSingleTransaction = createExpectSingleTransaction({ - anvil, - address: TEST_WALLET_ADDRESS, - options: { blocks: 2 }, + await expectSingleTransaction(async () => { + await page.getByTestId(TestID.ReviewSwap).click() + await expect(page.getByText('Unwrapped')).toBeVisible() + await expect(page.getByText('0.010 WETH for 0.010 ETH')).toBeVisible() + }) }) + test('should wrap ETH', async ({ page, anvil }) => { + const expectSingleTransaction = createExpectSingleTransaction({ + anvil, + address: TEST_WALLET_ADDRESS, + options: { blocks: 2 }, + }) - await stubTradingApiEndpoint({ page, endpoint: uniswapUrls.tradingApiPaths.swap }) + await stubTradingApiEndpoint({ page, endpoint: uniswapUrls.tradingApiPaths.swap }) - await page.goto(`/swap`) - await page.getByTestId(TestID.ChooseOutputToken).click() - // eslint-disable-next-line - await page.getByTestId('token-option-1-WETH').first().click() + await page.goto(`/swap`) + await page.getByTestId(TestID.ChooseOutputToken).click() + // eslint-disable-next-line + await page.getByTestId('token-option-1-WETH').first().click() - await page.getByTestId(TestID.AmountInputIn).click() - await page.getByTestId(TestID.AmountInputIn).fill('0.01') + await page.getByTestId(TestID.AmountInputIn).click() + await page.getByTestId(TestID.AmountInputIn).fill('0.01') - await expectSingleTransaction(async () => { - await page.getByTestId(TestID.ReviewSwap).click() - await expect(page.getByText('Wrapped')).toBeVisible() - await expect(page.getByText('0.010 ETH for 0.010 WETH')).toBeVisible() + await expectSingleTransaction(async () => { + await page.getByTestId(TestID.ReviewSwap).click() + await expect(page.getByText('Wrapped')).toBeVisible() + await expect(page.getByText('0.010 ETH for 0.010 WETH')).toBeVisible() + }) }) - }) -}) + }, +) diff --git a/apps/web/src/pages/Swap/common/shared.tsx b/apps/web/src/pages/Swap/common/shared.tsx index 12d3c508c3b..597a6a4ce09 100644 --- a/apps/web/src/pages/Swap/common/shared.tsx +++ b/apps/web/src/pages/Swap/common/shared.tsx @@ -1,6 +1,6 @@ import Row from 'components/deprecated/Row' import { Input, InputProps } from 'components/NumericalInput' -import styled, { css } from 'lib/styled-components' +import { css, deprecatedStyled } from 'lib/styled-components' import { useLayoutEffect, useState } from 'react' export const NumericalInputFontStyle = css<{ $fontSize?: number }>` @@ -10,13 +10,13 @@ export const NumericalInputFontStyle = css<{ $fontSize?: number }>` line-height: 60px; ` -export const NumericalInputWrapper = styled(Row)` +export const NumericalInputWrapper = deprecatedStyled(Row)` position: relative; max-width: 100%; width: max-content; ` -export const StyledNumericalInput = styled(Input)< +export const StyledNumericalInput = deprecatedStyled(Input)< { $width?: number; $hasPrefix?: boolean; $fontSize?: number } & InputProps >` max-height: 84px; @@ -30,7 +30,7 @@ export const StyledNumericalInput = styled(Input)< } ` -export const NumericalInputMimic = styled.span` +export const NumericalInputMimic = deprecatedStyled.span<{ $fontSize?: number }>` position: absolute; visibility: hidden; bottom: 0px; @@ -38,7 +38,7 @@ export const NumericalInputMimic = styled.span` ${NumericalInputFontStyle} ` -export const NumericalInputSymbolContainer = styled.span<{ showPlaceholder: boolean; $fontSize?: number }>` +export const NumericalInputSymbolContainer = deprecatedStyled.span<{ showPlaceholder: boolean; $fontSize?: number }>` user-select: none; color: ${({ theme }) => theme.neutral1}; ${NumericalInputFontStyle} diff --git a/apps/web/src/pages/Swap/index.tsx b/apps/web/src/pages/Swap/index.tsx index a06928cb810..5fca94d2bca 100644 --- a/apps/web/src/pages/Swap/index.tsx +++ b/apps/web/src/pages/Swap/index.tsx @@ -17,8 +17,7 @@ import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' import { useLocation, useNavigate } from 'react-router' import { MultichainContextProvider } from 'state/multichain/MultichainContext' -import { useSwapCallback } from 'state/sagas/transactions/swapSaga' -import { useWrapCallback } from 'state/sagas/transactions/wrapSaga' +import { useSwapHandlers } from 'state/sagas/transactions/useSwapHandlers' import { useInitialCurrencyState } from 'state/swap/hooks' import { SwapAndLimitContextProvider } from 'state/swap/SwapContext' import type { CurrencyState } from 'state/swap/types' @@ -259,8 +258,7 @@ function UniversalSwapFlow({ const { pathname } = useLocation() const navigate = useNavigate() const { t } = useTranslation() - const swapCallback = useSwapCallback() - const wrapCallback = useWrapCallback() + const swapHandlers = useSwapHandlers() const LimitFormWrapper = useDeferredComponent(() => import('pages/Swap/Limit/LimitForm').then((module) => ({ @@ -346,7 +344,7 @@ function UniversalSwapFlow({ )} {currentTab === SwapTab.Swap && ( - + { return ( - + {t('testnet.unsupported')} diff --git a/apps/web/src/pages/TokenDetails/TokenDetails.e2e.test.ts b/apps/web/src/pages/TokenDetails/TokenDetails.e2e.test.ts index 403d0d66a90..90b929887d1 100644 --- a/apps/web/src/pages/TokenDetails/TokenDetails.e2e.test.ts +++ b/apps/web/src/pages/TokenDetails/TokenDetails.e2e.test.ts @@ -4,59 +4,72 @@ import { TestID } from 'uniswap/src/test/fixtures/testIDs' const test = getTest() -test.describe('Token Details', () => { - test('token with warning and low trading volume should have all information populated', async ({ page, graphql }) => { - await graphql.intercept('TokenWeb', Mocks.TokenWeb.token_warning, { - chain: 'ETHEREUM', - address: '0x1eFBB78C8b917f67986BcE54cE575069c0143681', +test.describe( + 'Token Details', + { + tag: '@team:apps-portfolio', + annotation: [ + { type: 'DD_TAGS[team]', description: 'apps-portfolio' }, + { type: 'DD_TAGS[test.type]', description: 'web-e2e' }, + ], + }, + () => { + test('token with warning and low trading volume should have all information populated', async ({ + page, + graphql, + }) => { + await graphql.intercept('TokenWeb', Mocks.TokenWeb.token_warning, { + chain: 'ETHEREUM', + address: '0x1eFBB78C8b917f67986BcE54cE575069c0143681', + }) + await graphql.intercept('Token', Mocks.Token.token_warning, { + chain: 'ETHEREUM', + address: '0x1eFBB78C8b917f67986BcE54cE575069c0143681', + }) + await graphql.intercept('TokenProjects', Mocks.TokenProjects.token_spam, { + contracts: [ + { + chain: 'ETHEREUM', + address: '0x1eFBB78C8b917f67986BcE54cE575069c0143681', + }, + ], + }) + await page.goto('/explore/tokens/ethereum/0x1eFBB78C8b917f67986BcE54cE575069c0143681') + await expect(page.getByText('test token')).toBeVisible() + await expect(page.getByText('Missing chart data')).toBeVisible() + await expect(page.getByText('No stats available')).toBeVisible() + await expect(page.getByText('No token information available')).toBeVisible() }) - await graphql.intercept('Token', Mocks.Token.token_warning, { - chain: 'ETHEREUM', - address: '0x1eFBB78C8b917f67986BcE54cE575069c0143681', - }) - await graphql.intercept('TokenProjects', Mocks.TokenProjects.token_spam, { - contracts: [ - { - chain: 'ETHEREUM', - address: '0x1eFBB78C8b917f67986BcE54cE575069c0143681', - }, - ], - }) - await page.goto('/explore/tokens/ethereum/0x1eFBB78C8b917f67986BcE54cE575069c0143681') - await expect(page.getByText('test token')).toBeVisible() - await expect(page.getByText('Missing chart data')).toBeVisible() - await expect(page.getByText('No stats available')).toBeVisible() - await expect(page.getByText('No token information available')).toBeVisible() - }) - - test('disconnected wallet on mainnet mode should load mainnet token details', async ({ page }) => { - await page.goto('/explore/tokens/ethereum/NATIVE?eagerlyConnect=false') - await expect(page.getByText('Ethereum').first()).toBeVisible() - }) - test('connected wallet on mainnet mode should load testnet token details', async ({ page, graphql }) => { - await graphql.intercept('TokenWeb', Mocks.TokenWeb.sepolia_yay_token, { - chain: 'ETHEREUM_SEPOLIA', - address: '0x97dbb794244e1c27b6ff688fc8cef5fe8d80f531', + test('disconnected wallet on mainnet mode should load mainnet token details', async ({ page }) => { + await page.goto('/explore/tokens/ethereum/NATIVE?eagerlyConnect=false') + await expect(page.getByText('Ethereum').first()).toBeVisible() }) - await graphql.intercept('Token', Mocks.Token.sepolia_yay_token, { - chain: 'ETHEREUM_SEPOLIA', - address: '0x97dbb794244e1c27b6ff688fc8cef5fe8d80f531', + + test('connected wallet on mainnet mode should load testnet token details', async ({ page, graphql }) => { + await graphql.intercept('TokenWeb', Mocks.TokenWeb.sepolia_yay_token, { + chain: 'ETHEREUM_SEPOLIA', + address: '0x97dbb794244e1c27b6ff688fc8cef5fe8d80f531', + }) + await graphql.intercept('Token', Mocks.Token.sepolia_yay_token, { + chain: 'ETHEREUM_SEPOLIA', + address: '0x97dbb794244e1c27b6ff688fc8cef5fe8d80f531', + }) + await page.goto('/explore/tokens/ethereum_sepolia/0x97dbb794244e1c27b6ff688fc8cef5fe8d80f531') + await expect(page.getByText('Yay').first()).toBeVisible() }) - await page.goto('/explore/tokens/ethereum_sepolia/0x97dbb794244e1c27b6ff688fc8cef5fe8d80f531') - await expect(page.getByText('Yay').first()).toBeVisible() - }) - test('connected wallet on testnet mode should load mainnet token details', async ({ page }) => { - await page.goto('/explore/tokens/ethereum/NATIVE') - await page.getByTestId(TestID.Web3StatusConnected).click() - await page.getByTestId(TestID.WalletSettings).click() - await page.getByTestId(TestID.TestnetsToggle).click() - await expect(page.getByText('Ethereum').first()).toBeVisible() - }) + test('connected wallet on testnet mode should load mainnet token details', async ({ page }) => { + await page.goto('/explore/tokens/ethereum/NATIVE') + await page.getByTestId(TestID.Web3StatusConnected).click() + await page.getByTestId(TestID.WalletSettings).click() + await page.getByTestId(TestID.TestnetsToggle).click() + await expect(page.getByText('Ethereum').first()).toBeVisible() + }) - test('redirect to explore if token is not found', async ({ page }) => { - await page.goto('/explore/tokens/ethereum/0x123') - await expect(page).toHaveURL('/explore') - }) -}) + test('redirect to explore if token is not found', async ({ page }) => { + await page.goto('/explore/tokens/ethereum/0x123') + await expect(page).toHaveURL('/explore') + }) + }, +) diff --git a/apps/web/src/pages/Wrapped/DisconnectedState.tsx b/apps/web/src/pages/Wrapped/DisconnectedState.tsx new file mode 100644 index 00000000000..1c3be75f7c1 --- /dev/null +++ b/apps/web/src/pages/Wrapped/DisconnectedState.tsx @@ -0,0 +1,144 @@ +import { useAccountDrawer } from 'components/AccountDrawer/MiniPortfolio/hooks' +import { RefObject, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { Button, Flex, styled, Text, useWindowDimensions } from 'ui/src' +import { Gift } from 'ui/src/components/icons/Gift' +import { useSporeColorsForTheme } from 'ui/src/hooks/useSporeColors' +import { + MouseGlow, + renderSnowflakesWeb, + SnowflakeContainer, +} from 'uniswap/src/components/banners/shared/SharedSnowflakeComponents' +import { useSnowflakeAnimation } from 'uniswap/src/hooks/useSnowflakeAnimation' +import { isMobileWeb } from 'utilities/src/platform' + +const DisconnectedContainer = styled(Flex, { + width: '100%', + height: '100%', + position: 'relative', + justifyContent: 'center', + alignItems: 'center', + overflow: 'hidden', +}) + +const GradientBackground = styled(Flex, { + position: 'absolute', + inset: 0, + background: 'linear-gradient(180deg, #131313 0%, #3A123B 100%)', +}) + +const GlowEffect = styled(Flex, { + position: 'absolute', + left: 0, + bottom: 0, + width: '100%', + height: 30, + background: '#fc74fe', + filter: 'blur(90px)', + opacity: 0.4, +}) + +const IconWrapper = styled(Flex, { + width: 64, + height: 64, + borderRadius: '$rounded20', + backgroundColor: 'rgba(255, 255, 255, 0.1)', + backdropFilter: 'blur(8px)', + alignItems: 'center', + justifyContent: 'center', +}) + +export function DisconnectedState({ parentRef }: { parentRef: RefObject }): JSX.Element { + const [containerWidth, setContainerWidth] = useState(0) + const [containerHeight, setContainerHeight] = useState(0) + const { open: openAccountDrawer } = useAccountDrawer() + const { t } = useTranslation() + const darkColors = useSporeColorsForTheme('dark') + const { width: windowWidth, height: windowHeight } = useWindowDimensions() + + // set initital container height and width + useEffect(() => { + const rect = parentRef.current?.getBoundingClientRect() + setContainerWidth(rect?.width ?? windowWidth * 0.8) + setContainerHeight(rect?.height ?? windowHeight * 0.8) + }, [parentRef, windowWidth, windowHeight]) + + const { snowflakes, removeSnowflake, mouseInteraction } = useSnowflakeAnimation( + { + enabled: !isMobileWeb, + containerWidth, + bannerHeight: containerHeight, + }, + 0.5, + ) + + return ( + // biome-ignore lint/correctness/noRestrictedElements: Web-only mouse tracking for glow effect +
+ + + + + {/* Mouse-following glow effect */} + {mouseInteraction?.mousePosition && ( + + )} + + + {renderSnowflakesWeb({ + snowflakes, + containerHeight, + removeSnowflake, + getSnowflakeDrift: mouseInteraction?.getSnowflakeDrift, + keyPrefix: 'wrapped-disconnected', + })} + + + + + + + + + + {t('home.banner.uniswapWrapped2025.title')} + + + + {t('home.banner.uniswapWrapped2025.description')} + + + + + + + +
+ ) +} diff --git a/apps/web/src/pages/Wrapped/index.tsx b/apps/web/src/pages/Wrapped/index.tsx new file mode 100644 index 00000000000..623cb7135c9 --- /dev/null +++ b/apps/web/src/pages/Wrapped/index.tsx @@ -0,0 +1,130 @@ +import { DisconnectedState } from 'pages/Wrapped/DisconnectedState' +import { useEffect, useRef } from 'react' +import { useNavigate } from 'react-router' +import { useAppDispatch } from 'state/hooks' +import { Flex, TouchableArea } from 'ui/src' +import { X } from 'ui/src/components/icons/X' +import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions' +import { useSporeColorsForTheme } from 'ui/src/hooks/useSporeColors' +import { INTERFACE_NAV_HEIGHT, opacify } from 'ui/src/theme' +import { WRAPPED_PATH } from 'uniswap/src/components/banners/shared/utils' +import { useUrlContext } from 'uniswap/src/contexts/UrlContext' +import { useActiveAddresses } from 'uniswap/src/features/accounts/store/hooks' +import { setHasDismissedUniswapWrapped2025Banner } from 'uniswap/src/features/behaviorHistory/slice' +import { isAddress } from 'viem' + +export default function Wrapped() { + const { useParsedQueryString } = useUrlContext() + const queryParams = useParsedQueryString() + const walletAddressRef = useRef(undefined) + const backupWalletAddress = useActiveAddresses().evmAddress + const navigate = useNavigate() + const dispatch = useAppDispatch() + const containerRef = useRef(null) + const { fullWidth, fullHeight } = useDeviceDimensions() + const isLandscape = fullWidth > fullHeight + const darkColors = useSporeColorsForTheme('dark') + + // clear the query params after storing the wallet address + useEffect(() => { + const addressFromQuery = queryParams.address as string + if (addressFromQuery) { + if (isAddress(addressFromQuery)) { + walletAddressRef.current = addressFromQuery + } + navigate(WRAPPED_PATH, { replace: true }) + } + }, [queryParams.address, navigate]) + + // no longer show promo banner after viewing wrapped page + useEffect(() => { + dispatch(setHasDismissedUniswapWrapped2025Banner(true)) + }, [dispatch]) + + const hasWallet = Boolean(walletAddressRef.current || backupWalletAddress) + const walletAddress = walletAddressRef.current || backupWalletAddress + const iframeUrl = `https://wrapped.uniswap.org${walletAddress ? `?address=${walletAddress}` : ''}` + + return ( + + { + e.preventDefault() + e.stopPropagation() + }} + ref={containerRef} + > + {hasWallet ? ( +