diff --git a/.github/workflows/macstadium-builds.yml b/.github/workflows/macstadium-builds.yml new file mode 100644 index 00000000000..555ec762658 --- /dev/null +++ b/.github/workflows/macstadium-builds.yml @@ -0,0 +1,111 @@ +name: iOS builds +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + workflow_dispatch: + +jobs: + # Job to install dependencies + build: + runs-on: ["self-hosted"] + if: github.event.pull_request.draft == false + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + permissions: + contents: read + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Set up github keys + run: git config core.sshCommand "ssh -i ~/.ssh/id_ed25519 -F /dev/null" + + - name: Clean iOS app + run: yarn clean:ios > /dev/null 2>&1 || true + + - name: Set up ENV vars & scripts + env: + CI_SCRIPTS: ${{ secrets.CI_SCRIPTS }} + run: | + source ~/.zshrc + git clone git@github.com:rainbow-me/rainbow-env.git + mv rainbow-env/dotenv .env && rm -rf rainbow-env + eval $CI_SCRIPTS + + - name: Get Yarn cache directory path + id: yarn-cache-dir-path + run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT + + - name: Cache Yarn dependencies + uses: actions/cache@v4 + with: + path: | + ${{ steps.yarn-cache-dir-path.outputs.dir }} + .yarn/cache + .yarn/install-state.gz + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Install dependencies + run: | + yarn install && yarn setup + + - name: Version debug + run: | + npx react-native info + + - name: Install pods + run: yarn install-bundle && yarn install-pods + + - uses: irgaly/xcode-cache@v1 + with: + key: xcode-cache-deriveddata-${{ github.workflow }}-${{ github.sha }} + restore-keys: xcode-cache-deriveddata-${{ github.workflow }}- + + # TOPHAT iOS SIM + - name: Build the app in release mode for simulator + run: | + sed -i'' -e "s/IS_TESTING=true/IS_TESTING=false/" .env && rm -f .env-e + sed -i '' 's/match AppStore/match AdHoc/g' "ios/Rainbow.xcodeproj/project.pbxproj" + + xcodebuild -workspace ios/Rainbow.xcworkspace -scheme Rainbow -configuration Release -sdk iphonesimulator -derivedDataPath ios/build + APP_DIR=$(find . -name "*.app" | head -n 1) + cd $APP_DIR && zip -r ../../../../../../App.zip . + + # TOPHAT iOS DEVICE + - name: Build the app in release mode for iOS devices + env: + FASTLANE_USER: ${{ secrets.FASTLANE_USER }} + FASTLANE_PASSWORD: ${{ secrets.FASTLANE_PASSWORD }} + MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} + run: | + cd ios && bundle exec fastlane ios build_device + - name: Upload builds to AWS S3 + env: + AWS_BUCKET: rainbow-app-team-production + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + BRANCH_NAME: ${{ github.head_ref || github.ref_name }} + COMMIT_HASH: ${{ github.sha }} + run: | + APP_FILE=$(find . -name "App.zip" | head -n 1) + aws s3 cp "${APP_FILE}" "s3://${AWS_BUCKET}/${BRANCH_NAME}/${COMMIT_HASH}.app.zip" + IPA_FILE=./ios/build/Rainbow.ipa + aws s3 cp "${IPA_FILE}" "s3://${AWS_BUCKET}/${BRANCH_NAME}/${COMMIT_HASH}.ipa" + + - name: Post comment to PR + if: github.event_name == 'pull_request' + env: + TOPHAT_GITHUB_TOKEN: ${{ secrets.TOPHAT_GITHUB_TOKEN }} + BRANCH_NAME: ${{ github.head_ref || github.ref_name }} + COMMIT_HASH: ${{ github.sha }} + run: | + COMMENT="Launch in [simulator](http://localhost:29070/install/ios?virtual=https://app-team.p.rainbow.me/${BRANCH_NAME}/${COMMIT_HASH}.app.zip) or [device](http://localhost:29070/install/ios?physical=https://app-team.p.rainbow.me/${BRANCH_NAME}/${COMMIT_HASH}.ipa) for ${COMMIT_HASH}" + curl -s -H "Authorization: token $TOPHAT_GITHUB_TOKEN" -X POST \ + -d "{\"body\":\"$COMMENT\"}" \ + "${{ github.event.pull_request.comments_url }}" + + + diff --git a/.github/workflows/macstadium-e2e.yml b/.github/workflows/macstadium-tests.yml similarity index 64% rename from .github/workflows/macstadium-e2e.yml rename to .github/workflows/macstadium-tests.yml index fd826523eaa..dae6d82cc72 100644 --- a/.github/workflows/macstadium-e2e.yml +++ b/.github/workflows/macstadium-tests.yml @@ -1,16 +1,15 @@ name: iOS e2e tests - on: [pull_request, workflow_dispatch] + jobs: - ios-e2e: + # Job to install dependencies + install-deps: runs-on: ["self-hosted"] concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true - permissions: contents: read - steps: - name: Checkout repo uses: actions/checkout@v4 @@ -29,8 +28,7 @@ jobs: git clone git@github.com:rainbow-me/rainbow-env.git mv rainbow-env/dotenv .env && rm -rf rainbow-env eval $CI_SCRIPTS - sed -i'' -e "s/IS_TESTING=false/IS_TESTING=true/" .env && rm -f .env-e - + - name: Get Yarn cache directory path id: yarn-cache-dir-path run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT @@ -48,7 +46,26 @@ jobs: - name: Install dependencies run: | - yarn cache clean --all && yarn install && yarn setup + yarn install && yarn setup + + - name: Upload Yarn cache + uses: actions/upload-artifact@v3 + with: + name: yarn-cache + path: | + .yarn/cache + .yarn/install-state.gz + + # Job for linting and unit tests + linting-and-unit-tests: + runs-on: ["self-hosted"] + needs: install-deps + steps: + - name: Download Yarn cache + uses: actions/download-artifact@v3 + with: + name: yarn-cache + path: .yarn - name: Check for frozen lockfile run: ./scripts/check-lockfile.sh @@ -62,24 +79,36 @@ jobs: - name: Unit tests run: yarn test + # iOS build and e2e tests + e2e-ios: + runs-on: ["self-hosted"] + needs: install-deps + steps: + - name: Download Yarn cache + uses: actions/download-artifact@v3 + with: + name: yarn-cache + path: .yarn + - name: Rebuild detox cache run: ./node_modules/.bin/detox clean-framework-cache && ./node_modules/.bin/detox build-framework-cache - + - name: Version debug run: | npx react-native info - + - name: Install pods run: yarn install-bundle && yarn install-pods - - - name: Fix permissions - run: | - chmod -R +x node_modules/react-native/scripts - chmod -R +x node_modules/@sentry/react-native/scripts - - - name: Build the app in release mode - run: yarn detox build --configuration ios.sim.release - - # change the '3' here to how many times you want the tests to rerun on failure + + - uses: irgaly/xcode-cache@v1 + with: + key: xcode-cache-deriveddata-${{ github.workflow }}-${{ github.sha }} + restore-keys: xcode-cache-deriveddata-${{ github.workflow }}- + + # Detox iOS e2e tests - name: Run iOS e2e tests with retry - run: ./scripts/run-retry-tests.sh 3 + run: | + sed -i'' -e "s/IS_TESTING=false/IS_TESTING=true/" .env && rm -f .env-e + yarn detox build --configuration ios.sim.release + ./scripts/run-retry-tests.sh 3 + \ No newline at end of file diff --git a/android/app/src/main/java/me/rainbow/MainApplication.kt b/android/app/src/main/java/me/rainbow/MainApplication.kt index c096db243ff..08d0640db6a 100644 --- a/android/app/src/main/java/me/rainbow/MainApplication.kt +++ b/android/app/src/main/java/me/rainbow/MainApplication.kt @@ -21,6 +21,7 @@ import me.rainbow.NativeModules.RNStartTime.RNStartTimePackage import me.rainbow.NativeModules.RNTextAnimatorPackage.RNTextAnimatorPackage import me.rainbow.NativeModules.RNZoomableButton.RNZoomableButtonPackage import me.rainbow.NativeModules.SystemNavigationBar.SystemNavigationBarPackage +import me.rainbow.NativeModules.NavbarHeight.NavbarHeightPackage class MainApplication : Application(), ReactApplication { override val reactNativeHost: ReactNativeHost = object : DefaultReactNativeHost(this) { @@ -41,6 +42,7 @@ class MainApplication : Application(), ReactApplication { packages.add(KeychainPackage(KeychainModuleBuilder().withoutWarmUp())) packages.add(RNStartTimePackage(START_MARK)) packages.add(RNHapticsPackage()) + packages.add(NavbarHeightPackage()) return packages } diff --git a/android/app/src/main/java/me/rainbow/NativeModules/NavbarHeight/NavbarHeightModule.java b/android/app/src/main/java/me/rainbow/NativeModules/NavbarHeight/NavbarHeightModule.java new file mode 100644 index 00000000000..2a5884a4311 --- /dev/null +++ b/android/app/src/main/java/me/rainbow/NativeModules/NavbarHeight/NavbarHeightModule.java @@ -0,0 +1,91 @@ +package me.rainbow.NativeModules.NavbarHeight; + +import androidx.annotation.NonNull; + +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.module.annotations.ReactModule; +import android.graphics.Point; +import android.view.WindowManager; +import android.view.Display; +import java.lang.IllegalAccessException; +import java.lang.reflect.InvocationTargetException; +import java.lang.NoSuchMethodException; +import android.view.WindowInsets; +import android.os.Build; +import android.content.Context; + +@ReactModule(name = NavbarHeightModule.NAME) +public class NavbarHeightModule extends ReactContextBaseJavaModule { + public static final String NAME = "NavbarHeight"; + + public NavbarHeightModule(ReactApplicationContext reactContext) { + super(reactContext); + } + + @Override + @NonNull + public String getName() { + return NAME; + } + + // Example method + // See https://reactnative.dev/docs/native-modules-android + @ReactMethod + public double getNavigationBarHeightSync() { + Context context = getReactApplicationContext(); + WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + if (Build.VERSION.SDK_INT >= 30) { + return windowManager + .getCurrentWindowMetrics() + .getWindowInsets() + .getInsets(WindowInsets.Type.navigationBars()) + .bottom; + } else { + Point appUsableSize = getAppUsableScreenSize(context); + Point realScreenSize = getRealScreenSize(context); + + // navigation bar on the side + if (appUsableSize.x < realScreenSize.x) { + return appUsableSize.y; + } + + // navigation bar at the bottom + if (appUsableSize.y < realScreenSize.y) { + return realScreenSize.y - appUsableSize.y; + } + + // navigation bar is not present + return 0; + } + } + public Point getAppUsableScreenSize(Context context) { + WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + Display display = windowManager.getDefaultDisplay(); + Point size = new Point(); + display.getSize(size); + return size; + } + public Point getRealScreenSize(Context context) { + WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + Display display = windowManager.getDefaultDisplay(); + Point size = new Point(); + + if (Build.VERSION.SDK_INT >= 17) { + display.getRealSize(size); + } else if (Build.VERSION.SDK_INT >= 14) { + try { + size.x = (Integer) Display.class.getMethod("getRawWidth").invoke(display); + size.y = (Integer) Display.class.getMethod("getRawHeight").invoke(display); + } catch (IllegalAccessException e) {} catch (InvocationTargetException e) {} catch (NoSuchMethodException e) {} + } + + return size; + } + @ReactMethod(isBlockingSynchronousMethod = true) + public double getNavigationBarHeight() { + return getNavigationBarHeightSync(); + } +} \ No newline at end of file diff --git a/android/app/src/main/java/me/rainbow/NativeModules/NavbarHeight/NavbarHeightPackage.java b/android/app/src/main/java/me/rainbow/NativeModules/NavbarHeight/NavbarHeightPackage.java new file mode 100644 index 00000000000..0f4df602636 --- /dev/null +++ b/android/app/src/main/java/me/rainbow/NativeModules/NavbarHeight/NavbarHeightPackage.java @@ -0,0 +1,28 @@ +package me.rainbow.NativeModules.NavbarHeight; + +import androidx.annotation.NonNull; + +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.ViewManager; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class NavbarHeightPackage implements ReactPackage { + @NonNull + @Override + public List createNativeModules(@NonNull ReactApplicationContext reactContext) { + List modules = new ArrayList<>(); + modules.add(new NavbarHeightModule(reactContext)); + return modules; + } + + @NonNull + @Override + public List createViewManagers(@NonNull ReactApplicationContext reactContext) { + return Collections.emptyList(); + } +} \ No newline at end of file diff --git a/e2e/9_swaps.spec.ts b/e2e/9_swaps.spec.ts index f899fbdc68f..4655c019275 100644 --- a/e2e/9_swaps.spec.ts +++ b/e2e/9_swaps.spec.ts @@ -20,10 +20,8 @@ import { afterAllcleanApp, fetchElementAttributes, tap, - tapByText, delayTime, swipeUntilVisible, - tapAndLongPressByText, tapAndLongPress, swipe, } from './helpers'; @@ -69,10 +67,11 @@ describe('Swap Sheet Interaction Flow', () => { await tap('token-to-buy-dai-1'); await delayTime('medium'); + const swapInput = await fetchElementAttributes('swap-asset-input'); - expect(swapInput.label).toContain('ETH'); expect(swapInput.label).toContain('10'); + expect(swapInput.label).toContain('ETH'); }); it('Should be able to go to review and execute a swap', async () => { diff --git a/e2e/helpers.ts b/e2e/helpers.ts index dd82d8f2754..7ecb9811fc3 100644 --- a/e2e/helpers.ts +++ b/e2e/helpers.ts @@ -472,6 +472,11 @@ export async function sendETHtoTestWallet() { to: TESTING_WALLET, value: parseEther('20'), }); + await delayTime('long'); + const balance = await provider.getBalance(TESTING_WALLET); + if (balance.lt(parseEther('20'))) { + throw Error('Error sending ETH to test wallet'); + } return true; } diff --git a/globals.d.ts b/globals.d.ts index 29d1107dba2..068c5a86e74 100644 --- a/globals.d.ts +++ b/globals.d.ts @@ -109,4 +109,5 @@ declare module 'react-native-dotenv' { export const REACT_NATIVE_RUDDERSTACK_WRITE_KEY: string; export const RUDDERSTACK_DATA_PLANE_URL: string; export const SILENCE_EMOJI_WARNINGS: boolean; + export const MWP_ENCRYPTION_KEY: string; } diff --git a/ios/Gemfile.lock b/ios/Gemfile.lock index ca3dbe8fdae..1af4554f95c 100644 --- a/ios/Gemfile.lock +++ b/ios/Gemfile.lock @@ -1,37 +1,40 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.6) + CFPropertyList (3.0.7) + base64 + nkf rexml activesupport (7.0.5.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) 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.882.0) - aws-sdk-core (3.190.3) + aws-partitions (1.969.0) + aws-sdk-core (3.202.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) - aws-sigv4 (~> 1.8) + aws-sigv4 (~> 1.9) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.76.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) + aws-sdk-kms (1.88.0) + aws-sdk-core (~> 3, >= 3.201.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.159.0) + aws-sdk-core (~> 3, >= 3.201.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.8) - aws-sigv4 (1.8.0) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.9.1) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) + base64 (0.2.0) claide (1.1.0) cocoapods (1.14.3) addressable (~> 2.8) @@ -84,7 +87,7 @@ GEM escape (0.0.4) ethon (0.16.0) ffi (>= 1.15.0) - excon (0.109.0) + excon (0.111.0) faraday (1.10.3) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) @@ -106,22 +109,22 @@ GEM faraday-httpclient (1.0.1) faraday-multipart (1.0.4) multipart-post (~> 2) - faraday-net_http (1.0.1) + 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 (~> 1.0) - fastimage (2.3.0) - fastlane (2.219.0) + fastimage (2.3.1) + fastlane (2.222.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) aws-sdk-s3 (~> 1.0) babosa (>= 1.0.3, < 2.0.0) bundler (>= 1.12.0, < 3.0.0) - colored + colored (~> 1.2) commander (~> 4.6) dotenv (>= 2.1.1, < 3.0.0) emoji_regex (>= 0.1, < 4.0) @@ -142,10 +145,10 @@ GEM mini_magick (>= 4.9.4, < 5.0.0) multipart-post (>= 2.0.0, < 3.0.0) naturally (~> 2.2) - optparse (>= 0.1.1) + optparse (>= 0.1.1, < 1.0.0) plist (>= 3.1.0, < 4.0.0) rubyzip (>= 2.0.0, < 3.0.0) - security (= 0.1.3) + security (= 0.1.5) simctl (~> 1.6.3) terminal-notifier (>= 2.0.0, < 3.0.0) terminal-table (~> 3) @@ -154,7 +157,7 @@ GEM word_wrap (~> 1.0.0) xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.3.0) - xcpretty-travis-formatter (>= 0.0.3) + xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) ffi (1.16.3) fourflusher (2.3.1) fuzzy_match (2.0.4) @@ -175,12 +178,12 @@ GEM google-apis-core (>= 0.11.0, < 2.a) google-apis-storage_v1 (0.31.0) google-apis-core (>= 0.11.0, < 2.a) - google-cloud-core (1.6.1) + google-cloud-core (1.7.1) google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) google-cloud-env (1.6.0) faraday (>= 0.17.3, < 3.0) - google-cloud-errors (1.3.1) + google-cloud-errors (1.4.0) google-cloud-storage (1.47.0) addressable (~> 2.8) digest-crc (~> 0.4) @@ -196,41 +199,44 @@ GEM os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) highline (2.0.3) - http-cookie (1.0.5) + http-cookie (1.0.7) domain_name (~> 0.5) httpclient (2.8.3) i18n (1.14.1) concurrent-ruby (~> 1.0) jmespath (1.6.2) - json (2.7.1) - jwt (2.7.1) - mini_magick (4.12.0) + json (2.7.2) + jwt (2.8.2) + base64 + mini_magick (4.13.2) mini_mime (1.1.5) minitest (5.18.1) molinillo (0.8.0) multi_json (1.15.0) - multipart-post (2.3.0) + multipart-post (2.4.1) nanaimo (0.3.0) nap (1.1.0) naturally (2.2.1) netrc (0.11.0) - optparse (0.4.0) + nkf (0.2.0) + optparse (0.5.0) os (1.1.4) plist (3.7.1) public_suffix (4.0.7) - rake (13.1.0) + rake (13.2.1) 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.2.6) + rexml (3.3.6) + strscan rouge (2.0.7) ruby-macho (2.5.1) ruby2_keywords (0.0.5) rubyzip (2.3.2) - security (0.1.3) - signet (0.18.0) + security (0.1.5) + signet (0.19.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) jwt (>= 1.5, < 3.0) @@ -238,6 +244,7 @@ GEM simctl (1.6.10) CFPropertyList naturally + strscan (3.1.0) terminal-notifier (2.0.0) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) @@ -253,13 +260,13 @@ GEM uber (0.1.0) unicode-display_width (2.5.0) word_wrap (1.0.0) - xcodeproj (1.23.0) + xcodeproj (1.25.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) nanaimo (~> 0.3.0) - rexml (~> 3.2.4) + rexml (>= 3.3.2, < 4.0) xcpretty (0.3.0) rouge (~> 2.0.7) xcpretty-travis-formatter (1.0.1) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 1c400b0f1c1..f28ca0bae70 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -4,6 +4,9 @@ PODS: - BVLinearGradient (2.8.3): - React-Core - CocoaAsyncSocket (7.6.5) + - CoinbaseWalletSDK/Client (1.1.0) + - CoinbaseWalletSDK/Host (1.1.0): + - CoinbaseWalletSDK/Client - DoubleConversion (1.1.6) - FasterImage (1.6.2): - FasterImage/Nuke (= 1.6.2) @@ -171,6 +174,9 @@ PODS: - MMKV (1.3.9): - MMKVCore (~> 1.3.9) - MMKVCore (1.3.9) + - mobile-wallet-protocol-host (0.1.7): + - CoinbaseWalletSDK/Host + - React-Core - MultiplatformBleAdapter (0.1.9) - nanopb (2.30910.0): - nanopb/decode (= 2.30910.0) @@ -1826,6 +1832,7 @@ DEPENDENCIES: - GoogleUtilities - hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) - libwebp + - "mobile-wallet-protocol-host (from `../node_modules/@coinbase/mobile-wallet-protocol-host`)" - nanopb - PanModal (from `https://github.com/rainbow-me/PanModal`, commit `ab97d74279ba28c2891b47a5dc767ed4dd7cf994`) - Permission-Camera (from `../node_modules/react-native-permissions/ios/Camera`) @@ -1956,6 +1963,7 @@ SPEC REPOS: https://github.com/CocoaPods/Specs.git: - Branch - CocoaAsyncSocket + - CoinbaseWalletSDK - Firebase - FirebaseABTesting - FirebaseAnalytics @@ -2007,6 +2015,8 @@ EXTERNAL SOURCES: hermes-engine: :podspec: "../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec" :tag: hermes-2024-06-28-RNv0.74.3-7bda0c267e76d11b68a585f84cfdd65000babf85 + mobile-wallet-protocol-host: + :path: "../node_modules/@coinbase/mobile-wallet-protocol-host" PanModal: :commit: ab97d74279ba28c2891b47a5dc767ed4dd7cf994 :git: https://github.com/rainbow-me/PanModal @@ -2257,6 +2267,7 @@ SPEC CHECKSUMS: Branch: d99436c6f3d5b2529ba948d273e47e732830f207 BVLinearGradient: 880f91a7854faff2df62518f0281afb1c60d49a3 CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 + CoinbaseWalletSDK: bd6aa4f5a6460d4279e09e115969868e134126fb DoubleConversion: 76ab83afb40bddeeee456813d9c04f67f78771b5 FasterImage: af05a76f042ca3654c962b658fdb01cb4d31caee FBLazyVector: 7e977dd099937dc5458851233141583abba49ff2 @@ -2282,6 +2293,7 @@ SPEC CHECKSUMS: MetricsReporter: 99596ee5003c69949ed2f50acc34aee83c42f843 MMKV: 817ba1eea17421547e01e087285606eb270a8dcb MMKVCore: af055b00e27d88cd92fad301c5fecd1ff9b26dd9 + mobile-wallet-protocol-host: 8ed897dcf4f846d39b35767540e6a695631cab73 MultiplatformBleAdapter: 5a6a897b006764392f9cef785e4360f54fb9477d nanopb: 438bc412db1928dac798aa6fd75726007be04262 PanModal: 421fe72d4af5b7e9016aaa3b4db94a2fb71756d3 diff --git a/ios/Rainbow.xcodeproj/project.pbxproj b/ios/Rainbow.xcodeproj/project.pbxproj index e586a215e55..f7203347248 100644 --- a/ios/Rainbow.xcodeproj/project.pbxproj +++ b/ios/Rainbow.xcodeproj/project.pbxproj @@ -1652,12 +1652,14 @@ CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_IDENTITY = "iPhone Distribution"; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = L74NQAQB8H; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = L74NQAQB8H; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; @@ -1671,7 +1673,8 @@ MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = me.rainbow.ImageNotification; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = "match AppStore me.rainbow.ImageNotification"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore me.rainbow.ImageNotification"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_PRECOMPILE_BRIDGING_HEADER = NO; @@ -1864,11 +1867,13 @@ ASSETCATALOG_COMPILER_OPTIMIZATION = ""; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Rainbow/RainbowRelease.entitlements; - CODE_SIGN_IDENTITY = "iPhone Distribution"; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = L74NQAQB8H; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = L74NQAQB8H; ENABLE_BITCODE = NO; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; FRAMEWORK_SEARCH_PATHS = ( @@ -1905,7 +1910,8 @@ PRODUCT_BUNDLE_IDENTIFIER = me.rainbow; PRODUCT_NAME = Rainbow; PROVISIONING_PROFILE = ""; - PROVISIONING_PROFILE_SPECIFIER = "match AppStore me.rainbow"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore me.rainbow"; SWIFT_OBJC_BRIDGING_HEADER = "Rainbow-Bridging-Header.h"; SWIFT_PRECOMPILE_BRIDGING_HEADER = NO; SWIFT_VERSION = 5.0; @@ -2331,11 +2337,13 @@ CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_ENTITLEMENTS = PriceWidgetExtension.entitlements; - CODE_SIGN_IDENTITY = "iPhone Distribution"; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = L74NQAQB8H; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = L74NQAQB8H; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; @@ -2345,7 +2353,8 @@ MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = me.rainbow.PriceWidget; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = "match AppStore me.rainbow.PriceWidget"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore me.rainbow.PriceWidget"; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; SWIFT_PRECOMPILE_BRIDGING_HEADER = NO; @@ -2425,8 +2434,8 @@ CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_ENTITLEMENTS = PriceWidgetExtension.entitlements; - CODE_SIGN_IDENTITY = "iPhone Distribution"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = L74NQAQB8H; @@ -2439,7 +2448,7 @@ MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = me.rainbow.PriceWidget; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = "match AppStore me.rainbow.PriceWidget"; + PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; SWIFT_PRECOMPILE_BRIDGING_HEADER = NO; @@ -2517,11 +2526,13 @@ CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_ENTITLEMENTS = SelectTokenIntent/SelectTokenIntent.entitlements; - CODE_SIGN_IDENTITY = "iPhone Distribution"; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = L74NQAQB8H; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = L74NQAQB8H; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; @@ -2531,7 +2542,8 @@ MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = me.rainbow.SelectTokenIntent; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = "match AppStore me.rainbow.SelectTokenIntent"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore me.rainbow.SelectTokenIntent"; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; SWIFT_PRECOMPILE_BRIDGING_HEADER = NO; @@ -2607,8 +2619,8 @@ CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_ENTITLEMENTS = SelectTokenIntent/SelectTokenIntent.entitlements; - CODE_SIGN_IDENTITY = "iPhone Distribution"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = L74NQAQB8H; @@ -2621,7 +2633,7 @@ MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = me.rainbow.SelectTokenIntent; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = "match AppStore me.rainbow.SelectTokenIntent"; + PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; SWIFT_PRECOMPILE_BRIDGING_HEADER = NO; @@ -2711,7 +2723,8 @@ CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_IDENTITY = "iPhone Distribution"; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; @@ -2929,7 +2942,8 @@ CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_IDENTITY = "iPhone Distribution"; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; diff --git a/ios/Rainbow/Info.plist b/ios/Rainbow/Info.plist index 4d31e1cc358..8273cab6e66 100644 --- a/ios/Rainbow/Info.plist +++ b/ios/Rainbow/Info.plist @@ -2,8 +2,6 @@ - RCTAsyncStorageExcludeFromBackup - CADisableMinimumFrameDurationOnPhone CFBundleDevelopmentRegion @@ -16,122 +14,122 @@ CFBundleAlternateIcons - pooly + adworld - UIPrerenderedIcon - CFBundleIconFiles - pooly + adworld - - poolboy - UIPrerenderedIcon - CFBundleIconFiles - - poolboy - - smol + farcaster - UIPrerenderedIcon - CFBundleIconFiles - smol + farcaster - - raindoge - UIPrerenderedIcon + + finiliar + CFBundleIconFiles - raindoge + finiliar + UIPrerenderedIcon + golddoge - UIPrerenderedIcon - CFBundleIconFiles golddoge - - pixel - UIPrerenderedIcon + + og + CFBundleIconFiles - pixel + og + UIPrerenderedIcon + optimism - UIPrerenderedIcon - CFBundleIconFiles optimism - - zora - UIPrerenderedIcon + + pixel + CFBundleIconFiles - zora + pixel - - finiliar - UIPrerenderedIcon + + poolboy + CFBundleIconFiles - finiliar + poolboy - - zorb - UIPrerenderedIcon + + pooly + CFBundleIconFiles - zorb + pooly - - farcaster - UIPrerenderedIcon + + raindoge + CFBundleIconFiles - farcaster + raindoge - - adworld - UIPrerenderedIcon + + smol + CFBundleIconFiles - adworld + smol + UIPrerenderedIcon + - og + zora + CFBundleIconFiles + + zora + UIPrerenderedIcon + + zorb + CFBundleIconFiles - og + zorb + UIPrerenderedIcon + CFBundlePrimaryIcon @@ -187,9 +185,13 @@ NSAllowsArbitraryLoads + NSAllowsArbitraryLoadsInWebContent + NSAllowsLocalNetworking + NSBluetoothAlwaysUsageDescription + This allows Rainbow to find and pair a Ledger Nano X NSCameraUsageDescription This lets you scan QR codes for connecting to dApps and discovering wallets NSFaceIDUsageDescription @@ -198,8 +200,6 @@ This allows Rainbow to save images to your Camera Roll NSPhotoLibraryUsageDescription This allows Rainbow to display selected photos as avatars across each of your wallet addresses - NSBluetoothAlwaysUsageDescription - This allows Rainbow to find and pair a Ledger Nano X NSUbiquitousContainers iCloud.me.rainbow @@ -216,6 +216,8 @@ SelectTokenIntent + RCTAsyncStorageExcludeFromBackup + UIAppFonts SF-Mono-Bold.otf diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index a3191ae123b..eafd2c4db2c 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -23,10 +23,6 @@ platform :ios do duration: 1200, # optional in_house: false, # optional but may be required if using match/sigh ) - - upload_symbols_to_crashlytics( - dsym_path: './Rainbow.app.dSYM.zip' - ) end desc "Submit a new Beta build to Apple TestFlight" @@ -67,7 +63,18 @@ platform :ios do scheme: "Rainbow", include_symbols: true, configuration: "Release", - export_method: "app-store" + export_method: "app-store", + export_options: { + signingStyle: "manual", + provisioningProfiles: { + "me.rainbow" => "match AppStore me.rainbow", + "me.rainbow.PriceWidget" => "match AppStore me.rainbow.PriceWidget", + "me.rainbow.SelectTokenIntent" => "match AppStore me.rainbow.SelectTokenIntent", + "me.rainbow.ImageNotification" => "match AppStore me.rainbow.ImageNotification", + "me.rainbow.OpenInRainbow" => "match AppStore me.rainbow.OpenInRainbow", + "me.rainbow.ShareWithRainbow" => "match AppStore me.rainbow.ShareWithRainbow", + }, + } ) pilot( @@ -77,4 +84,47 @@ platform :ios do ) end + desc "Create new build for iOS devices in release mode" + lane :build_device do + + update_info_plist( + plist_path: "Rainbow/Info.plist", + display_name: "Rainbow" + ) + + update_app_identifier( + app_identifier: "me.rainbow", + xcodeproj: "Rainbow.xcodeproj", + plist_path: "Rainbow/Info.plist", + ) + + match( + force: true, + type: "adhoc", + app_identifier: ["me.rainbow", "me.rainbow.PriceWidget", "me.rainbow.SelectTokenIntent", "me.rainbow.ImageNotification", "me.rainbow.OpenInRainbow", "me.rainbow.ShareWithRainbow"], + git_url: "git@github.com:rainbow-me/rainbow-code-signing.git", + ) + + gym( + workspace: "Rainbow.xcworkspace", + scheme: "Rainbow", + include_symbols: false, + export_method: "ad-hoc", + output_directory: "build", + output_name: "Rainbow.ipa", + archive_path: "build/Rainbow.xcarchive", + export_options: { + signingStyle: "manual", + provisioningProfiles: { + "me.rainbow" => "match AdHoc me.rainbow", + "me.rainbow.PriceWidget" => "match AdHoc me.rainbow.PriceWidget", + "me.rainbow.SelectTokenIntent" => "match AdHoc me.rainbow.SelectTokenIntent", + "me.rainbow.ImageNotification" => "match AdHoc me.rainbow.ImageNotification", + "me.rainbow.OpenInRainbow" => "match AdHoc me.rainbow.OpenInRainbow", + "me.rainbow.ShareWithRainbow" => "match AdHoc me.rainbow.ShareWithRainbow", + }, + } + ) + + end end diff --git a/package.json b/package.json index 533313474f1..37c442c9cde 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "@bradgarropy/use-countdown": "1.4.1", "@candlefinance/faster-image": "1.6.2", "@capsizecss/core": "3.0.0", + "@coinbase/mobile-wallet-protocol-host": "0.1.7", "@ensdomains/address-encoder": "0.2.16", "@ensdomains/content-hash": "2.5.7", "@ensdomains/eth-ens-namehash": "2.0.15", @@ -193,7 +194,6 @@ "moti": "0.28", "multiformats": "9.6.2", "nanoid": "3.2.0", - "p-queue": "7.2.0", "p-wait-for": "4.1.0", "pako": "2.0.4", "parse-ms": "2.1.0", diff --git a/src/App.tsx b/src/App.tsx index 02c836a846e..c2ea134d17b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,40 +1,31 @@ -import './languages'; +import '@/languages'; import * as Sentry from '@sentry/react-native'; -import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { AppRegistry, AppState, AppStateStatus, Dimensions, InteractionManager, Linking, LogBox, View } from 'react-native'; -import branch from 'react-native-branch'; - +import React, { useCallback, useEffect, useState } from 'react'; +import { AppRegistry, Dimensions, LogBox, StyleSheet, View } from 'react-native'; +import { MobileWalletProtocolProvider } from '@coinbase/mobile-wallet-protocol-host'; +import { DeeplinkHandler } from '@/components/DeeplinkHandler'; +import { AppStateChangeHandler } from '@/components/AppStateChangeHandler'; +import { useApplicationSetup } from '@/hooks/useApplicationSetup'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import { enableScreens } from 'react-native-screens'; import { connect, Provider as ReduxProvider } from 'react-redux'; import { RecoilRoot } from 'recoil'; -import PortalConsumer from './components/PortalConsumer'; -import ErrorBoundary from './components/error-boundary/ErrorBoundary'; -import { OfflineToast } from './components/toasts'; -import { designSystemPlaygroundEnabled, reactNativeDisableYellowBox, showNetworkRequests, showNetworkResponses } from './config/debug'; -import monitorNetwork from './debugging/network'; -import { Playground } from './design-system/playground/Playground'; -import handleDeeplink from './handlers/deeplinks'; -import { runWalletBackupStatusChecks } from './handlers/walletReadyEvents'; -import RainbowContextWrapper from './helpers/RainbowContext'; -import isTestFlight from './helpers/isTestFlight'; +import PortalConsumer from '@/components/PortalConsumer'; +import ErrorBoundary from '@/components/error-boundary/ErrorBoundary'; +import { OfflineToast } from '@/components/toasts'; +import { designSystemPlaygroundEnabled, reactNativeDisableYellowBox, showNetworkRequests, showNetworkResponses } from '@/config/debug'; +import monitorNetwork from '@/debugging/network'; +import { Playground } from '@/design-system/playground/Playground'; +import RainbowContextWrapper from '@/helpers/RainbowContext'; import * as keychain from '@/model/keychain'; -import { loadAddress } from './model/wallet'; -import { Navigation } from './navigation'; -import RoutesComponent from './navigation/Routes'; -import { PerformanceContextMap } from './performance/PerformanceContextMap'; -import { PerformanceTracking } from './performance/tracking'; -import { PerformanceMetrics } from './performance/tracking/types/PerformanceMetrics'; -import { PersistQueryClientProvider, persistOptions, queryClient } from './react-query'; -import store from './redux/store'; -import { walletConnectLoadState } from './redux/walletconnect'; -import { MainThemeProvider } from './theme/ThemeContext'; -import { branchListener } from './utils/branch'; -import { addressKey } from './utils/keychainConstants'; +import { Navigation } from '@/navigation'; +import { PersistQueryClientProvider, persistOptions, queryClient } from '@/react-query'; +import store, { AppDispatch, type AppState } from '@/redux/store'; +import { MainThemeProvider } from '@/theme/ThemeContext'; +import { addressKey } from '@/utils/keychainConstants'; import { SharedValuesProvider } from '@/helpers/SharedValuesContext'; -import { InitialRoute, InitialRouteContext } from '@/navigation/initialRoute'; -import Routes from '@/navigation/routesNames'; +import { InitialRouteContext } from '@/navigation/initialRoute'; import { Portal } from '@/react-native-cool-modals/Portal'; import { NotificationsHandler } from '@/notifications/NotificationsHandler'; import { analyticsV2 } from '@/analytics'; @@ -42,18 +33,15 @@ import { getOrCreateDeviceId, securelyHashWalletAddress } from '@/analytics/util import { logger, RainbowError } from '@/logger'; import * as ls from '@/storage'; import { migrate } from '@/migrations'; -import { initListeners as initWalletConnectListeners } from '@/walletConnect'; -import { saveFCMToken } from '@/notifications/tokens'; import { initializeReservoirClient } from '@/resources/reservoir/client'; import { ReviewPromptAction } from '@/storage/schema'; -import { handleReviewPromptAction } from '@/utils/reviewAlert'; import { initializeRemoteConfig } from '@/model/remoteConfig'; import { NavigationContainerRef } from '@react-navigation/native'; -import { RootStackParamList } from './navigation/types'; +import { RootStackParamList } from '@/navigation/types'; import { Address } from 'viem'; -import { IS_DEV } from './env'; -import { checkIdentifierOnLaunch } from './model/backup'; -import { prefetchDefaultFavorites } from './resources/favorites'; +import { IS_DEV } from '@/env'; +import { prefetchDefaultFavorites } from '@/resources/favorites'; +import Routes from '@/navigation/Routes'; if (IS_DEV) { reactNativeDisableYellowBox && LogBox.ignoreAllLogs(); @@ -62,139 +50,49 @@ if (IS_DEV) { enableScreens(); -const containerStyle = { flex: 1 }; +const sx = StyleSheet.create({ + container: { + flex: 1, + }, +}); interface AppProps { walletReady: boolean; } function App({ walletReady }: AppProps) { - const [appState, setAppState] = useState(AppState.currentState); - const [initialRoute, setInitialRoute] = useState(null); - const eventSubscription = useRef | null>(null); - const branchListenerRef = useRef | null>(null); - const navigatorRef = useRef | null>(null); - - const setupDeeplinking = useCallback(async () => { - const initialUrl = await Linking.getInitialURL(); - - branchListenerRef.current = await branchListener(url => { - logger.debug(`[App]: Branch: listener called`, {}, logger.DebugContext.deeplinks); - try { - handleDeeplink(url, initialRoute); - } catch (error) { - if (error instanceof Error) { - logger.error(new RainbowError(`[App]: Error opening deeplink`), { - message: error.message, - url, - }); - } else { - logger.error(new RainbowError(`[App]: Error opening deeplink`), { - message: 'Unknown error', - url, - }); - } - } - }); - - if (initialUrl) { - logger.debug(`[App]: has initial URL, opening with Branch`, { initialUrl }); - branch.openURL(initialUrl); - } - }, [initialRoute]); - - const identifyFlow = useCallback(async () => { - const address = await loadAddress(); - if (address) { - setTimeout(() => { - InteractionManager.runAfterInteractions(() => { - handleReviewPromptAction(ReviewPromptAction.TimesLaunchedSinceInstall); - }); - }, 10_000); - - InteractionManager.runAfterInteractions(checkIdentifierOnLaunch); - } - - setInitialRoute(address ? Routes.SWIPE_LAYOUT : Routes.WELCOME_SCREEN); - PerformanceContextMap.set('initialRoute', address ? Routes.SWIPE_LAYOUT : Routes.WELCOME_SCREEN); - }, []); - - const handleAppStateChange = useCallback( - (nextAppState: AppStateStatus) => { - if (appState === 'background' && nextAppState === 'active') { - store.dispatch(walletConnectLoadState()); - } - setAppState(nextAppState); - analyticsV2.track(analyticsV2.event.appStateChange, { - category: 'app state', - label: nextAppState, - }); - }, - [appState] - ); + const { initialRoute } = useApplicationSetup(); const handleNavigatorRef = useCallback((ref: NavigationContainerRef) => { - navigatorRef.current = ref; Navigation.setTopLevelNavigator(ref); }, []); - useEffect(() => { - if (!__DEV__ && isTestFlight) { - logger.debug(`[App]: Test flight usage - ${isTestFlight}`); - } - identifyFlow(); - eventSubscription.current = AppState.addEventListener('change', handleAppStateChange); - - const p1 = analyticsV2.initializeRudderstack(); - const p2 = setupDeeplinking(); - const p3 = saveFCMToken(); - Promise.all([p1, p2, p3]).then(() => { - initWalletConnectListeners(); - PerformanceTracking.finishMeasuring(PerformanceMetrics.loadRootAppComponent); - analyticsV2.track(analyticsV2.event.applicationDidMount); - }); - - return () => { - eventSubscription.current?.remove(); - branchListenerRef.current?.(); - }; - }, []); - - useEffect(() => { - if (walletReady) { - logger.debug(`[App]: ✅ Wallet ready!`); - runWalletBackupStatusChecks(); - } - }, [walletReady]); - return ( - + {initialRoute && ( - + )} + + ); } -export type AppStore = typeof store; -export type RootState = ReturnType; -export type AppDispatch = AppStore['dispatch']; - -const AppWithRedux = connect(state => ({ +const AppWithRedux = connect(state => ({ walletReady: state.appState.walletReady, }))(App); function Root() { - const [initializing, setInitializing] = React.useState(true); + const [initializing, setInitializing] = useState(true); - React.useEffect(() => { + useEffect(() => { async function initializeApplication() { await initializeRemoteConfig(); await migrate(); @@ -300,19 +198,21 @@ function Root() { prefetchDefaultFavorites(); }} > - - - - - - - - - - - - - + + + + + + + + + + + + + + + diff --git a/src/__swaps__/screens/Swap/Swap.tsx b/src/__swaps__/screens/Swap/Swap.tsx index 7f005ef3095..f04d1e23b9f 100644 --- a/src/__swaps__/screens/Swap/Swap.tsx +++ b/src/__swaps__/screens/Swap/Swap.tsx @@ -18,7 +18,7 @@ import { SwapInputAsset } from '@/__swaps__/screens/Swap/components/SwapInputAss import { SwapNavbar } from '@/__swaps__/screens/Swap/components/SwapNavbar'; import { SwapOutputAsset } from '@/__swaps__/screens/Swap/components/SwapOutputAsset'; import { SwapSheetGestureBlocker } from '@/__swaps__/screens/Swap/components/SwapSheetGestureBlocker'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; import { SwapAssetType } from '@/__swaps__/types/swap'; import { parseSearchAsset } from '@/__swaps__/utils/assets'; import { AbsolutePortalRoot } from '@/components/AbsolutePortal'; @@ -164,7 +164,8 @@ const WalletAddressObserver = () => { if (didWalletAddressChange) { runOnJS(setNewInputAsset)(); } - } + }, + [] ); return null; diff --git a/src/__swaps__/screens/Swap/components/AnimatedChainImage.android.tsx b/src/__swaps__/screens/Swap/components/AnimatedChainImage.android.tsx index 96873dce266..689f9374507 100644 --- a/src/__swaps__/screens/Swap/components/AnimatedChainImage.android.tsx +++ b/src/__swaps__/screens/Swap/components/AnimatedChainImage.android.tsx @@ -13,7 +13,7 @@ const AvalancheBadge = require('@/assets/badges/avalanche.png'); const BlastBadge = require('@/assets/badges/blast.png'); const DegenBadge = require('@/assets/badges/degen.png'); -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; import { globalColors } from '@/design-system'; import { PIXEL_RATIO } from '@/utils/deviceUtils'; import { useSwapsStore } from '@/state/swaps/swapsStore'; diff --git a/src/__swaps__/screens/Swap/components/AnimatedChainImage.ios.tsx b/src/__swaps__/screens/Swap/components/AnimatedChainImage.ios.tsx index ceae4f7d750..7e34b1ac740 100644 --- a/src/__swaps__/screens/Swap/components/AnimatedChainImage.ios.tsx +++ b/src/__swaps__/screens/Swap/components/AnimatedChainImage.ios.tsx @@ -11,18 +11,15 @@ import ZoraBadge from '@/assets/badges/zora.png'; import AvalancheBadge from '@/assets/badges/avalanche.png'; import BlastBadge from '@/assets/badges/blast.png'; import DegenBadge from '@/assets/badges/degen.png'; -import { ChainId } from '@/__swaps__/types/chains'; -import { useAnimatedProps } from 'react-native-reanimated'; -import { AddressOrEth } from '@/__swaps__/types/assets'; +import { ChainId } from '@/networks/types'; +import { useAnimatedProps, useDerivedValue } from 'react-native-reanimated'; import { AnimatedFasterImage } from '@/components/AnimatedComponents/AnimatedFasterImage'; import { DEFAULT_FASTER_IMAGE_CONFIG } from '@/components/images/ImgixImage'; import { globalColors } from '@/design-system'; -import { customChainIdsToAssetNames } from '@/__swaps__/utils/chains'; -import { AddressZero } from '@ethersproject/constants'; -import { ETH_ADDRESS } from '@/references'; import { IS_ANDROID } from '@/env'; import { PIXEL_RATIO } from '@/utils/deviceUtils'; import { useSwapContext } from '../providers/swap-provider'; +import { BLANK_BASE64_PIXEL } from '@/components/DappBrowser/constants'; const networkBadges = { [ChainId.mainnet]: Image.resolveAssetSource(EthereumBadge).uri, @@ -47,19 +44,6 @@ const networkBadges = { [ChainId.degen]: Image.resolveAssetSource(DegenBadge).uri, }; -const getCustomChainIconUrlWorklet = (chainId: ChainId, address: AddressOrEth) => { - 'worklet'; - - if (!chainId || !customChainIdsToAssetNames[chainId]) return ''; - const baseUrl = 'https://raw.githubusercontent.com/rainbow-me/assets/master/blockchains/'; - - if (address === AddressZero || address === ETH_ADDRESS) { - return `${baseUrl}${customChainIdsToAssetNames[chainId]}/info/logo.png`; - } else { - return `${baseUrl}${customChainIdsToAssetNames[chainId]}/assets/${address}/logo.png`; - } -}; - export function AnimatedChainImage({ assetType, showMainnetBadge = false, @@ -70,42 +54,28 @@ export function AnimatedChainImage({ size?: number; }) { const { internalSelectedInputAsset, internalSelectedOutputAsset } = useSwapContext(); - const asset = assetType === 'input' ? internalSelectedInputAsset : internalSelectedOutputAsset; - - const animatedIconSource = useAnimatedProps(() => { - const base = { - source: { - ...DEFAULT_FASTER_IMAGE_CONFIG, - borderRadius: IS_ANDROID ? (size / 2) * PIXEL_RATIO : size / 2, - url: '', - }, - }; - if (!asset?.value) { - if (!showMainnetBadge) { - return base; - } - base.source.url = networkBadges[ChainId.mainnet]; - return base; - } + const url = useDerivedValue(() => { + const asset = assetType === 'input' ? internalSelectedInputAsset : internalSelectedOutputAsset; + const chainId = asset?.value?.chainId; - if (networkBadges[asset.value.chainId]) { - if (!showMainnetBadge && asset.value.chainId === ChainId.mainnet) { - return base; - } - base.source.url = networkBadges[asset.value.chainId]; - return base; - } + let url = 'eth'; - const url = getCustomChainIconUrlWorklet(asset.value.chainId, asset.value.address); - if (url) { - base.source.url = url; - return base; + if (chainId !== undefined && !(!showMainnetBadge && chainId === ChainId.mainnet)) { + url = networkBadges[chainId]; } - - return base; + return url; }); + const animatedIconSource = useAnimatedProps(() => ({ + source: { + ...DEFAULT_FASTER_IMAGE_CONFIG, + base64Placeholder: BLANK_BASE64_PIXEL, + borderRadius: IS_ANDROID ? (size / 2) * PIXEL_RATIO : size / 2, + url: url.value, + }, + })); + return ( {/* ⚠️ TODO: This works but we should figure out how to type this correctly to avoid this error */} diff --git a/src/__swaps__/screens/Swap/components/AnimatedSwapCoinIcon.tsx b/src/__swaps__/screens/Swap/components/AnimatedSwapCoinIcon.tsx index 230b007c130..cb9314825e3 100644 --- a/src/__swaps__/screens/Swap/components/AnimatedSwapCoinIcon.tsx +++ b/src/__swaps__/screens/Swap/components/AnimatedSwapCoinIcon.tsx @@ -41,69 +41,54 @@ export const AnimatedSwapCoinIcon = memo(function FeedCoinIcon({ showBadge?: boolean; }) { const { isDarkMode, colors } = useTheme(); - const { internalSelectedInputAsset, internalSelectedOutputAsset } = useSwapContext(); + const asset = assetType === 'input' ? internalSelectedInputAsset : internalSelectedOutputAsset; + const size = small ? 16 : large ? 36 : 32; const didErrorForUniqueId = useSharedValue(undefined); - const size = small ? 16 : large ? 36 : 32; - // Shield animated props from unnecessary updates to avoid flicker - const coinIconUrl = useDerivedValue(() => asset.value?.icon_url ?? ''); + const coinIconUrl = useDerivedValue(() => asset.value?.icon_url || ''); const animatedIconSource = useAnimatedProps(() => { return { source: { ...DEFAULT_FASTER_IMAGE_CONFIG, borderRadius: IS_ANDROID ? (size / 2) * PIXEL_RATIO : undefined, - transitionDuration: 0, url: coinIconUrl.value, }, }; }); - const animatedCoinIconWrapperStyles = useAnimatedStyle(() => { - const showEmptyState = !asset.value?.uniqueId; - const showFallback = didErrorForUniqueId.value === asset.value?.uniqueId; - const shouldDisplay = !showFallback && !showEmptyState; - - return { - shadowColor: shouldDisplay ? (isDarkMode ? colors.shadow : asset.value?.shadowColor['light']) : 'transparent', - }; - }); - - const animatedCoinIconStyles = useAnimatedStyle(() => { - const showEmptyState = !asset.value?.uniqueId; - const showFallback = didErrorForUniqueId.value === asset.value?.uniqueId; - const shouldDisplay = !showFallback && !showEmptyState; - - return { - display: shouldDisplay ? 'flex' : 'none', - pointerEvents: shouldDisplay ? 'auto' : 'none', - opacity: withTiming(shouldDisplay ? 1 : 0, fadeConfig), - }; - }); - - const animatedEmptyStateStyles = useAnimatedStyle(() => { + const visibility = useDerivedValue(() => { const showEmptyState = !asset.value?.uniqueId; + const showFallback = !showEmptyState && (didErrorForUniqueId.value === asset.value?.uniqueId || !asset.value?.icon_url); + const showCoinIcon = !showFallback && !showEmptyState; - return { - display: showEmptyState ? 'flex' : 'none', - opacity: withTiming(showEmptyState ? 1 : 0, fadeConfig), - }; + return { showCoinIcon, showEmptyState, showFallback }; }); - const animatedFallbackStyles = useAnimatedStyle(() => { - const showEmptyState = !asset.value?.uniqueId; - const showFallback = !showEmptyState && didErrorForUniqueId.value === asset.value?.uniqueId; - - return { - display: showFallback ? 'flex' : 'none', - pointerEvents: showFallback ? 'auto' : 'none', - opacity: withTiming(showFallback ? 1 : 0, fadeConfig), - }; - }); + const animatedCoinIconWrapperStyles = useAnimatedStyle(() => ({ + shadowColor: visibility.value.showCoinIcon ? (isDarkMode ? colors.shadow : asset.value?.shadowColor['light']) : 'transparent', + })); + + const animatedCoinIconStyles = useAnimatedStyle(() => ({ + display: visibility.value.showCoinIcon ? 'flex' : 'none', + pointerEvents: visibility.value.showCoinIcon ? 'auto' : 'none', + opacity: withTiming(visibility.value.showCoinIcon ? 1 : 0, fadeConfig), + })); + + const animatedEmptyStateStyles = useAnimatedStyle(() => ({ + display: visibility.value.showEmptyState ? 'flex' : 'none', + opacity: withTiming(visibility.value.showEmptyState ? 1 : 0, fadeConfig), + })); + + const animatedFallbackStyles = useAnimatedStyle(() => ({ + display: visibility.value.showFallback ? 'flex' : 'none', + pointerEvents: visibility.value.showFallback ? 'auto' : 'none', + opacity: withTiming(visibility.value.showFallback ? 1 : 0, fadeConfig), + })); return ( diff --git a/src/__swaps__/screens/Swap/components/CoinRow.tsx b/src/__swaps__/screens/Swap/components/CoinRow.tsx index 0728370c7aa..37c7feb897c 100644 --- a/src/__swaps__/screens/Swap/components/CoinRow.tsx +++ b/src/__swaps__/screens/Swap/components/CoinRow.tsx @@ -1,14 +1,14 @@ import { BalancePill } from '@/__swaps__/screens/Swap/components/BalancePill'; import { CoinRowButton } from '@/__swaps__/screens/Swap/components/CoinRowButton'; import { AddressOrEth, ParsedSearchAsset } from '@/__swaps__/types/assets'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; import { SearchAsset } from '@/__swaps__/types/search'; import { ButtonPressAnimation } from '@/components/animations'; import { ContextMenuButton } from '@/components/context-menu'; import { Box, Column, Columns, HitSlop, Inline, Text } from '@/design-system'; import { setClipboard } from '@/hooks/useClipboard'; import * as i18n from '@/languages'; -import { RainbowNetworks } from '@/networks'; +import { RainbowNetworkObjects } from '@/networks'; import { BASE_DEGEN_ADDRESS, DEGEN_CHAIN_DEGEN_ADDRESS, ETH_ADDRESS } from '@/references'; import { toggleFavorite } from '@/resources/favorites'; import { userAssetsStore } from '@/state/assets/userAssets'; @@ -185,7 +185,7 @@ export function CoinRow({ isFavorite, onPress, output, uniqueId, testID, ...asse } const InfoButton = ({ address, chainId }: { address: string; chainId: ChainId }) => { - const network = RainbowNetworks.find(network => network.id === chainId)?.value; + const networkObject = RainbowNetworkObjects.find(networkObject => networkObject.id === chainId)?.value; const handleCopy = useCallback(() => { haptics.selection(); @@ -197,11 +197,11 @@ const InfoButton = ({ address, chainId }: { address: string; chainId: ChainId }) title: i18n.t(i18n.l.exchange.coin_row.copy_contract_address), action: handleCopy, }, - ...(network + ...(networkObject ? { blockExplorer: { - title: i18n.t(i18n.l.exchange.coin_row.view_on, { blockExplorerName: startCase(ethereumUtils.getBlockExplorer(chainId)) }), - action: () => ethereumUtils.openAddressInBlockExplorer(address, chainId), + title: i18n.t(i18n.l.exchange.coin_row.view_on, { blockExplorerName: startCase(ethereumUtils.getBlockExplorer({ chainId })) }), + action: () => ethereumUtils.openAddressInBlockExplorer({ address, chainId }), }, } : {}), @@ -217,7 +217,7 @@ const InfoButton = ({ address, chainId }: { address: string; chainId: ChainId }) iconValue: 'doc.on.doc', }, }, - ...(network + ...(networkObject ? [ { actionKey: 'blockExplorer', @@ -236,7 +236,7 @@ const InfoButton = ({ address, chainId }: { address: string; chainId: ChainId }) const handlePressMenuItem = async ({ nativeEvent: { actionKey } }: OnPressMenuItemEventObject) => { if (actionKey === 'copyAddress') { options.copy.action(); - } else if (actionKey === 'blockExplorer' && network) { + } else if (actionKey === 'blockExplorer' && networkObject) { options.blockExplorer?.action(); } }; @@ -244,14 +244,14 @@ const InfoButton = ({ address, chainId }: { address: string; chainId: ChainId }) const onPressAndroid = () => showActionSheetWithOptions( { - options: [options.copy.title, ...(network ? [options.blockExplorer?.title] : [])], + options: [options.copy.title, ...(networkObject ? [options.blockExplorer?.title] : [])], showSeparators: true, }, (idx: number) => { if (idx === 0) { options.copy.action(); } - if (idx === 1 && network) { + if (idx === 1 && networkObject) { options.blockExplorer?.action(); } } diff --git a/src/__swaps__/screens/Swap/components/ExchangeRateBubble.tsx b/src/__swaps__/screens/Swap/components/ExchangeRateBubble.tsx index 8f4de13dece..17a14866819 100644 --- a/src/__swaps__/screens/Swap/components/ExchangeRateBubble.tsx +++ b/src/__swaps__/screens/Swap/components/ExchangeRateBubble.tsx @@ -130,7 +130,8 @@ export const ExchangeRateBubble = () => { break; } } - } + }, + [] ); const bubbleVisibilityWrapper = useAnimatedStyle(() => { diff --git a/src/__swaps__/screens/Swap/components/FastSwapCoinIconImage.tsx b/src/__swaps__/screens/Swap/components/FastSwapCoinIconImage.tsx deleted file mode 100644 index ed659118401..00000000000 --- a/src/__swaps__/screens/Swap/components/FastSwapCoinIconImage.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import React from 'react'; -import { StyleSheet, View } from 'react-native'; -import { ImgixImage } from '@/components/images'; -import { Network } from '@/networks/types'; -import { getUrlForTrustIconFallback } from '@/utils'; - -export const FastSwapCoinIconImage = React.memo(function FastSwapCoinIconImage({ - address, - disableShadow = true, - network, - shadowColor, - size, -}: { - address: string; - children: () => React.ReactNode; - disableShadow?: boolean; - network: Network; - shadowColor: string; - size?: number; -}) { - const imageUrl = getUrlForTrustIconFallback(address, network); - - return ( - - - - ); -}); - -const sx = StyleSheet.create({ - coinIconContainer: { - alignItems: 'center', - borderRadius: 20, - height: 40, - justifyContent: 'center', - overflow: 'visible', - width: 40, - }, - coinIconFallback: { - borderRadius: 20, - height: 40, - overflow: 'hidden', - width: 40, - }, - container: { - elevation: 6, - height: 59, - overflow: 'visible', - paddingTop: 9, - }, - contract: { - height: 40, - width: 40, - }, - fallbackWrapper: { - left: 0, - position: 'absolute', - top: 0, - }, - reactCoinIconContainer: { - alignItems: 'center', - justifyContent: 'center', - }, - reactCoinIconImage: { - height: '100%', - width: '100%', - }, - withShadow: { - elevation: 6, - shadowOffset: { - height: 4, - width: 0, - }, - shadowOpacity: 0.2, - shadowRadius: 6, - }, -}); diff --git a/src/__swaps__/screens/Swap/components/FlipButton.tsx b/src/__swaps__/screens/Swap/components/FlipButton.tsx index 1bc69c1df28..0b71487dfcf 100644 --- a/src/__swaps__/screens/Swap/components/FlipButton.tsx +++ b/src/__swaps__/screens/Swap/components/FlipButton.tsx @@ -14,7 +14,7 @@ import { TIMING_CONFIGS } from '@/components/animations/animationConfigs'; import { SwapAssetType } from '@/__swaps__/types/swap'; import { GestureHandlerButton } from './GestureHandlerButton'; import { useSwapsStore } from '@/state/swaps/swapsStore'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; export const FlipButton = () => { const { isDarkMode } = useColorMode(); diff --git a/src/__swaps__/screens/Swap/components/GasButton.tsx b/src/__swaps__/screens/Swap/components/GasButton.tsx index bdef2deda67..6f96e02db53 100644 --- a/src/__swaps__/screens/Swap/components/GasButton.tsx +++ b/src/__swaps__/screens/Swap/components/GasButton.tsx @@ -1,4 +1,3 @@ -import { ChainId } from '@/__swaps__/types/chains'; import { GasSpeed } from '@/__swaps__/types/gas'; import { weiToGwei } from '@/__swaps__/utils/ethereum'; import { getCachedCurrentBaseFee, useMeteorologySuggestions } from '@/__swaps__/utils/meteorology'; @@ -24,6 +23,7 @@ import { NavigationSteps, useSwapContext } from '../providers/swap-provider'; import { EstimatedSwapGasFee, EstimatedSwapGasFeeSlot } from './EstimatedSwapGasFee'; import { GestureHandlerButton } from './GestureHandlerButton'; import { UnmountOnAnimatedReaction } from './UnmountOnAnimatedReaction'; +import { ChainId } from '@/networks/types'; const { SWAP_GAS_ICONS } = gasUtils; const GAS_BUTTON_HIT_SLOP = 16; diff --git a/src/__swaps__/screens/Swap/components/GasPanel.tsx b/src/__swaps__/screens/Swap/components/GasPanel.tsx index 77f6a7dbf0a..d7534155908 100644 --- a/src/__swaps__/screens/Swap/components/GasPanel.tsx +++ b/src/__swaps__/screens/Swap/components/GasPanel.tsx @@ -4,7 +4,7 @@ import Animated, { runOnJS, useAnimatedReaction, useAnimatedStyle, withDelay, wi import { MIN_FLASHBOTS_PRIORITY_FEE, THICK_BORDER_WIDTH } from '@/__swaps__/screens/Swap/constants'; import { NavigationSteps, useSwapContext } from '@/__swaps__/screens/Swap/providers/swap-provider'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; import { GasSpeed } from '@/__swaps__/types/gas'; import { gweiToWei, weiToGwei } from '@/__swaps__/utils/ethereum'; import { @@ -428,7 +428,8 @@ export function GasPanel() { if (previous === NavigationSteps.SHOW_GAS && current !== NavigationSteps.SHOW_GAS) { runOnJS(saveCustomGasSettings)(); } - } + }, + [] ); const styles = useAnimatedStyle(() => { diff --git a/src/__swaps__/screens/Swap/components/ReviewPanel.tsx b/src/__swaps__/screens/Swap/components/ReviewPanel.tsx index 9441ba77056..ecad3bde19a 100644 --- a/src/__swaps__/screens/Swap/components/ReviewPanel.tsx +++ b/src/__swaps__/screens/Swap/components/ReviewPanel.tsx @@ -1,9 +1,7 @@ import { AnimatedChainImage } from '@/__swaps__/screens/Swap/components/AnimatedChainImage'; import { ReviewGasButton } from '@/__swaps__/screens/Swap/components/GasButton'; import { GestureHandlerButton } from '@/__swaps__/screens/Swap/components/GestureHandlerButton'; -import { useNativeAssetForChain } from '@/__swaps__/screens/Swap/hooks/useNativeAssetForChain'; -import { ChainId, ChainNameDisplay } from '@/__swaps__/types/chains'; -import { chainNameFromChainId } from '@/__swaps__/utils/chains'; +import { ChainNameDisplay, ChainId } from '@/networks/types'; import { useEstimatedTime } from '@/__swaps__/utils/meteorology'; import { convertRawAmountToBalance, @@ -28,11 +26,9 @@ import { useColorMode, useForegroundColor, } from '@/design-system'; -import { useAccountSettings } from '@/hooks'; import * as i18n from '@/languages'; import { useNavigation } from '@/navigation'; import Routes from '@/navigation/routesNames'; -import { getNetworkObject } from '@/networks'; import { swapsStore, useSwapsStore } from '@/state/swaps/swapsStore'; import { getNativeAssetForNetwork } from '@/utils/ethereumUtils'; import { CrosschainQuote, Quote, QuoteError } from '@rainbow-me/swaps'; @@ -65,11 +61,8 @@ const MAX_SLIPPAGE_LABEL = i18n.t(i18n.l.exchange.slippage_tolerance); const ESTIMATED_NETWORK_FEE_LABEL = i18n.t(i18n.l.gas.network_fee); const RainbowFee = () => { - const { nativeCurrency } = useAccountSettings(); const { isDarkMode } = useColorMode(); - const { isFetching, isQuoteStale, quote, internalSelectedInputAsset } = useSwapContext(); - - const { nativeAsset } = useNativeAssetForChain({ inputAsset: internalSelectedInputAsset }); + const { isFetching, isQuoteStale, quote } = useSwapContext(); const index = useSharedValue(0); const rainbowFee = useSharedValue([UNKNOWN_LABEL, UNKNOWN_LABEL]); @@ -105,7 +98,8 @@ const RainbowFee = () => { if (!current.isQuoteStale && !current.isFetching && current.quote && !(current.quote as QuoteError)?.error) { runOnJS(calculateRainbowFeeFromQuoteData)(current.quote as Quote | CrosschainQuote); } - } + }, + [] ); return ( @@ -140,15 +134,10 @@ function EstimatedArrivalTime() { function FlashbotsToggle() { const { SwapSettings } = useSwapContext(); - const inputAssetChainId = swapsStore(state => state.inputAsset?.chainId) ?? ChainId.mainnet; - const isFlashbotsEnabledForNetwork = getNetworkObject({ chainId: inputAssetChainId }).features.flashbots; - const flashbotsToggleValue = useDerivedValue(() => isFlashbotsEnabledForNetwork && SwapSettings.flashbots.value); - return ( @@ -364,10 +353,10 @@ export function ReviewPanel() { }); const openGasExplainer = useCallback(async () => { - const nativeAsset = await getNativeAssetForNetwork(swapsStore.getState().inputAsset?.chainId ?? ChainId.mainnet); + const nativeAsset = await getNativeAssetForNetwork({ chainId: swapsStore.getState().inputAsset?.chainId ?? ChainId.mainnet }); navigate(Routes.EXPLAIN_SHEET, { - network: chainNameFromChainId(swapsStore.getState().inputAsset?.chainId ?? ChainId.mainnet), + chainId: swapsStore.getState().inputAsset?.chainId ?? ChainId.mainnet, type: 'gas', nativeAsset, }); diff --git a/src/__swaps__/screens/Swap/components/SearchInput.tsx b/src/__swaps__/screens/Swap/components/SearchInput.tsx index 8b215bf64f7..d764efacd92 100644 --- a/src/__swaps__/screens/Swap/components/SearchInput.tsx +++ b/src/__swaps__/screens/Swap/components/SearchInput.tsx @@ -70,7 +70,8 @@ export const SearchInput = ({ if (output) runOnJS(onOutputSearchQueryChange)(''); else runOnJS(onInputSearchQueryChange)(''); } - } + }, + [] ); return ( diff --git a/src/__swaps__/screens/Swap/components/SearchInputButton.tsx b/src/__swaps__/screens/Swap/components/SearchInputButton.tsx index 999b3ee9c48..30c5a60f673 100644 --- a/src/__swaps__/screens/Swap/components/SearchInputButton.tsx +++ b/src/__swaps__/screens/Swap/components/SearchInputButton.tsx @@ -9,6 +9,7 @@ import * as i18n from '@/languages'; import { THICK_BORDER_WIDTH } from '../constants'; import { useClipboard } from '@/hooks'; import { TIMING_CONFIGS } from '@/components/animations/animationConfigs'; +import { triggerHapticFeedback } from '@/screens/points/constants'; const CANCEL_LABEL = i18n.t(i18n.l.button.cancel); const CLOSE_LABEL = i18n.t(i18n.l.button.close); @@ -51,31 +52,45 @@ export const SearchInputButton = ({ return PASTE_LABEL; }); - const onPaste = useCallback(() => { - Clipboard.getString().then(text => { - // to prevent users from mistakingly pasting long ass texts when copying the wrong thing - // we slice the string to 42 which is the size of a eth address, - // no token name query search should be that big anyway - const v = text.trim().slice(0, 42); - pastedSearchInputValue.value = v; - useSwapsStore.setState({ outputSearchQuery: v }); - }); - }, []); + const onPaste = useCallback( + (isPasteDisabled: boolean) => { + if (isPasteDisabled) { + triggerHapticFeedback('notificationError'); + return; + } - const buttonVisibilityStyle = useAnimatedStyle(() => { + Clipboard.getString().then(text => { + // Slice the pasted text to the length of an ETH address + const v = text.trim().slice(0, 42); + pastedSearchInputValue.value = v; + useSwapsStore.setState({ outputSearchQuery: v }); + }); + }, + [pastedSearchInputValue] + ); + + const buttonInfo = useDerivedValue(() => { const isInputSearchFocused = inputProgress.value === NavigationSteps.SEARCH_FOCUSED; const isOutputSearchFocused = outputProgress.value === NavigationSteps.SEARCH_FOCUSED; + const isOutputTokenListFocused = outputProgress.value === NavigationSteps.TOKEN_LIST_FOCUSED; - const isVisible = - isInputSearchFocused || - isOutputSearchFocused || - (output && (internalSelectedOutputAsset.value || hasClipboardData)) || - (!output && internalSelectedInputAsset.value); + const isVisible = isInputSearchFocused || isOutputSearchFocused || output || (!output && !!internalSelectedInputAsset.value); + const isPasteDisabled = output && !internalSelectedOutputAsset.value && isOutputTokenListFocused && !hasClipboardData; + const visibleOpacity = isPasteDisabled ? 0.4 : 1; + + return { + isPasteDisabled, + isVisible, + visibleOpacity, + }; + }); + + const buttonVisibilityStyle = useAnimatedStyle(() => { return { - display: isVisible ? 'flex' : 'none', - opacity: isVisible ? withTiming(1, TIMING_CONFIGS.slowerFadeConfig) : 0, - pointerEvents: isVisible ? 'auto' : 'none', + display: buttonInfo.value.isVisible ? 'flex' : 'none', + opacity: buttonInfo.value.isVisible ? withTiming(buttonInfo.value.visibleOpacity, TIMING_CONFIGS.tabPressConfig) : 0, + pointerEvents: buttonInfo.value.isVisible ? 'auto' : 'none', }; }); @@ -88,7 +103,7 @@ export const SearchInputButton = ({ onPressWorklet={() => { 'worklet'; if (output && outputProgress.value === NavigationSteps.TOKEN_LIST_FOCUSED && !internalSelectedOutputAsset.value) { - runOnJS(onPaste)(); + runOnJS(onPaste)(buttonInfo.value.isPasteDisabled); } if (isSearchFocused.value || (output && internalSelectedOutputAsset.value) || (!output && internalSelectedInputAsset.value)) { diff --git a/src/__swaps__/screens/Swap/components/SwapActionButton.tsx b/src/__swaps__/screens/Swap/components/SwapActionButton.tsx index a6a8d913966..4d8f2dab126 100644 --- a/src/__swaps__/screens/Swap/components/SwapActionButton.tsx +++ b/src/__swaps__/screens/Swap/components/SwapActionButton.tsx @@ -139,7 +139,14 @@ function SwapButton({ )} {typeof label !== 'undefined' && ( - + {labelValue} @@ -187,7 +194,8 @@ const HoldProgress = ({ holdProgress }: { holdProgress: SharedValue }) = if (current && current !== previous) { runOnJS(transformColor)(getColorValueForThemeWorklet(current, isDarkMode, true)); } - } + }, + [] ); return ( diff --git a/src/__swaps__/screens/Swap/components/SwapBottomPanel.tsx b/src/__swaps__/screens/Swap/components/SwapBottomPanel.tsx index 87298a06eb8..1bb554d9617 100644 --- a/src/__swaps__/screens/Swap/components/SwapBottomPanel.tsx +++ b/src/__swaps__/screens/Swap/components/SwapBottomPanel.tsx @@ -22,7 +22,8 @@ import { SwapActionButton } from './SwapActionButton'; import { SettingsPanel } from './SettingsPanel'; import { SPRING_CONFIGS } from '@/components/animations/animationConfigs'; import { triggerHapticFeedback } from '@/screens/points/constants'; -import { LONG_PRESS_DURATION_IN_MS } from '@/components/buttons/hold-to-authorize/constants'; + +const HOLD_TO_SWAP_DURATION_MS = 400; export function SwapBottomPanel() { const { isDarkMode } = useColorMode(); @@ -104,6 +105,7 @@ export function SwapBottomPanel() { icon={icon} iconStyle={confirmButtonIconStyle} label={label} + longPressDuration={HOLD_TO_SWAP_DURATION_MS} disabled={disabled} onPressWorklet={() => { 'worklet'; @@ -132,7 +134,7 @@ export function SwapBottomPanel() { holdProgress.value = 0; holdProgress.value = withTiming( 100, - { duration: LONG_PRESS_DURATION_IN_MS, easing: Easing.inOut(Easing.sin) }, + { duration: HOLD_TO_SWAP_DURATION_MS, easing: Easing.inOut(Easing.sin) }, isFinished => { if (isFinished) { holdProgress.value = 0; diff --git a/src/__swaps__/screens/Swap/components/SwapCoinIcon.tsx b/src/__swaps__/screens/Swap/components/SwapCoinIcon.tsx index e206a0d0a85..7e26e38f4b8 100644 --- a/src/__swaps__/screens/Swap/components/SwapCoinIcon.tsx +++ b/src/__swaps__/screens/Swap/components/SwapCoinIcon.tsx @@ -10,7 +10,7 @@ import { FallbackIcon as CoinIconTextFallback, isETH } from '@/utils'; import { FastFallbackCoinIconImage } from '@/components/asset-list/RecyclerAssetList2/FastComponents/FastFallbackCoinIconImage'; import Animated from 'react-native-reanimated'; import FastImage, { Source } from 'react-native-fast-image'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; // TODO: Delete this and replace with RainbowCoinIcon // ⚠️ When replacing this component with RainbowCoinIcon, make sure diff --git a/src/__swaps__/screens/Swap/components/SwapOutputAsset.tsx b/src/__swaps__/screens/Swap/components/SwapOutputAsset.tsx index 74ffb1cc923..c2fbd147ea8 100644 --- a/src/__swaps__/screens/Swap/components/SwapOutputAsset.tsx +++ b/src/__swaps__/screens/Swap/components/SwapOutputAsset.tsx @@ -15,7 +15,7 @@ import { TokenList } from '@/__swaps__/screens/Swap/components/TokenList/TokenLi import { BASE_INPUT_WIDTH, INPUT_INNER_WIDTH, INPUT_PADDING, THICK_BORDER_WIDTH } from '@/__swaps__/screens/Swap/constants'; import { IS_ANDROID, IS_IOS } from '@/env'; import { useSwapContext } from '@/__swaps__/screens/Swap/providers/swap-provider'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; import * as i18n from '@/languages'; import { useNavigation } from '@/navigation'; import Routes from '@/navigation/routesNames'; @@ -59,15 +59,13 @@ function SwapOutputAmount() { const { inputAsset, outputAsset } = useSwapsStore.getState(); const inputTokenSymbol = inputAsset?.symbol; const outputTokenSymbol = outputAsset?.symbol; - const inputNetwork = ethereumUtils.getNetworkFromChainId(inputAsset?.chainId ?? ChainId.mainnet); - const outputNetwork = ethereumUtils.getNetworkFromChainId(outputAsset?.chainId ?? ChainId.mainnet); const isCrosschainSwap = inputAsset?.chainId !== outputAsset?.chainId; const isBridgeSwap = inputTokenSymbol === outputTokenSymbol; navigate(Routes.EXPLAIN_SHEET, { inputToken: inputTokenSymbol, - fromNetwork: inputNetwork, - toNetwork: outputNetwork, + fromChainId: inputAsset?.chainId ?? ChainId.mainnet, + toChainId: outputAsset?.chainId ?? ChainId.mainnet, isCrosschainSwap, isBridgeSwap, outputToken: outputTokenSymbol, @@ -81,7 +79,8 @@ function SwapOutputAmount() { v => { 'worklet'; runOnJS(setIsPasteEnabled)(v); - } + }, + [] ); return ( diff --git a/src/__swaps__/screens/Swap/components/SwapSlider.tsx b/src/__swaps__/screens/Swap/components/SwapSlider.tsx index 5919a88da4b..42581fd1087 100644 --- a/src/__swaps__/screens/Swap/components/SwapSlider.tsx +++ b/src/__swaps__/screens/Swap/components/SwapSlider.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useRef } from 'react'; import { StyleSheet, View } from 'react-native'; import * as i18n from '@/languages'; -import { PanGestureHandler, TapGestureHandler, TapGestureHandlerGestureEvent } from 'react-native-gesture-handler'; +import { PanGestureHandler, State, TapGestureHandler, TapGestureHandlerGestureEvent } from 'react-native-gesture-handler'; import Animated, { interpolate, interpolateColor, @@ -204,21 +204,27 @@ export const SwapSlider = ({ overshoot.value = calculateOvershoot(overshootX, maxOverscroll); } }, - onFinish: (event, ctx: { startX: number }) => { + onFinish: (event, ctx: { exceedsMax?: boolean; startX: number }) => { const onFinished = () => { overshoot.value = withSpring(0, SPRING_CONFIGS.sliderConfig); + if (xPercentage.value >= 0.995) { if (isQuoteStale.value === 1) { runOnJS(onChangeWrapper)(1); } sliderXPosition.value = withSpring(width, SPRING_CONFIGS.snappySpringConfig); + } else if (event.state === State.FAILED) { + SwapInputController.quoteFetchingInterval.start(); + return; } else if (xPercentage.value < 0.005) { runOnJS(onChangeWrapper)(0); sliderXPosition.value = withSpring(0, SPRING_CONFIGS.snappySpringConfig); isQuoteStale.value = 0; isFetching.value = false; - } else { + } else if (ctx.startX !== sliderXPosition.value) { runOnJS(onChangeWrapper)(xPercentage.value); + } else { + SwapInputController.quoteFetchingInterval.start(); } }; @@ -392,9 +398,15 @@ export const SwapSlider = ({ }); return ( - + - + @@ -419,8 +431,8 @@ export const SwapSlider = ({ {MAX_LABEL} diff --git a/src/__swaps__/screens/Swap/components/TokenList/ChainSelection.tsx b/src/__swaps__/screens/Swap/components/TokenList/ChainSelection.tsx index 5a4ade623e2..00c53e26585 100644 --- a/src/__swaps__/screens/Swap/components/TokenList/ChainSelection.tsx +++ b/src/__swaps__/screens/Swap/components/TokenList/ChainSelection.tsx @@ -6,7 +6,7 @@ import { Text as RNText, StyleSheet } from 'react-native'; import Animated, { useDerivedValue, useSharedValue } from 'react-native-reanimated'; import { useSwapContext } from '@/__swaps__/screens/Swap/providers/swap-provider'; -import { ChainId, ChainName, ChainNameDisplay } from '@/__swaps__/types/chains'; +import { ChainId, ChainNameDisplay } from '@/networks/types'; import { chainNameForChainIdWithMainnetSubstitution } from '@/__swaps__/utils/chains'; import { opacity } from '@/__swaps__/utils/swaps'; import { analyticsV2 } from '@/analytics'; @@ -78,7 +78,7 @@ export const ChainSelection = memo(function ChainSelection({ allText, output }: return { actionKey: `${chainId}`, - actionTitle: displayName, + actionTitle: displayName as string, icon: { iconType: 'ASSET', iconValue: `${networkName}Badge${chainId === ChainId.mainnet ? '' : 'NoShadow'}`, @@ -89,7 +89,7 @@ export const ChainSelection = memo(function ChainSelection({ allText, output }: if (!output) { supportedChains.unshift({ actionKey: 'all', - actionTitle: i18n.t(i18n.l.exchange.all_networks) as ChainName, + actionTitle: i18n.t(i18n.l.exchange.all_networks), icon: { iconType: 'icon', iconValue: '􀆪', diff --git a/src/__swaps__/screens/Swap/components/TokenList/TokenToBuyList.tsx b/src/__swaps__/screens/Swap/components/TokenList/TokenToBuyList.tsx index bb4d3b75c80..407e41d7fc6 100644 --- a/src/__swaps__/screens/Swap/components/TokenList/TokenToBuyList.tsx +++ b/src/__swaps__/screens/Swap/components/TokenList/TokenToBuyList.tsx @@ -1,8 +1,9 @@ +import { FlatList } from 'react-native'; import { COIN_ROW_WITH_PADDING_HEIGHT, CoinRow } from '@/__swaps__/screens/Swap/components/CoinRow'; import { ListEmpty } from '@/__swaps__/screens/Swap/components/TokenList/ListEmpty'; import { AssetToBuySectionId, useSearchCurrencyLists } from '@/__swaps__/screens/Swap/hooks/useSearchCurrencyLists'; import { useSwapContext } from '@/__swaps__/screens/Swap/providers/swap-provider'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; import { SearchAsset } from '@/__swaps__/types/search'; import { SwapAssetType } from '@/__swaps__/types/swap'; import { parseSearchAsset } from '@/__swaps__/utils/assets'; @@ -16,9 +17,7 @@ import * as i18n from '@/languages'; import { userAssetsStore } from '@/state/assets/userAssets'; import { swapsStore } from '@/state/swaps/swapsStore'; import { DEVICE_WIDTH } from '@/utils/deviceUtils'; -import { FlashList } from '@shopify/flash-list'; -import React, { ComponentType, forwardRef, memo, useCallback, useMemo } from 'react'; -import { ScrollViewProps } from 'react-native'; +import React, { memo, useCallback, useMemo } from 'react'; import Animated, { runOnUI, useAnimatedProps, useAnimatedStyle, withTiming } from 'react-native-reanimated'; import { EXPANDED_INPUT_HEIGHT, FOCUSED_INPUT_HEIGHT } from '../../constants'; import { ChainSelection } from './ChainSelection'; @@ -73,15 +72,25 @@ export type HeaderItem = { listItemType: 'header'; id: AssetToBuySectionId; data export type CoinRowItem = SearchAsset & { listItemType: 'coinRow'; sectionId: AssetToBuySectionId }; export type TokenToBuyListItem = HeaderItem | CoinRowItem; -const ScrollViewWithRef = forwardRef(function ScrollViewWithRef(props, ref) { - const { outputProgress } = useSwapContext(); - const animatedListProps = useAnimatedProps(() => { - const isFocused = outputProgress.value === 2; - return { scrollIndicatorInsets: { bottom: 28 + (isFocused ? EXPANDED_INPUT_HEIGHT - FOCUSED_INPUT_HEIGHT : 0) } }; - }); - // eslint-disable-next-line react/jsx-props-no-spreading - return ; -}); +const getItemLayout = (data: ArrayLike | null | undefined, index: number) => { + if (!data) return { length: 0, offset: 0, index }; + + const item = data[index]; + const length = item?.listItemType === 'header' ? BUY_LIST_HEADER_HEIGHT : COIN_ROW_WITH_PADDING_HEIGHT; + + // Count headers up to this index + let headerCount = 0; + for (let i = 0; i < index; i++) { + if (data[i]?.listItemType === 'header') { + headerCount += 1; + } + } + + const coinRowCount = index - headerCount; + const offset = headerCount * BUY_LIST_HEADER_HEIGHT + coinRowCount * COIN_ROW_WITH_PADDING_HEIGHT; + + return { length, offset, index }; +}; export const TokenToBuyList = () => { const { internalSelectedInputAsset, internalSelectedOutputAsset, isFetching, isQuoteStale, outputProgress, setAsset } = useSwapContext(); @@ -130,12 +139,12 @@ export const TokenToBuyList = () => { return { height: bottomPadding }; }); - const averageItemSize = useMemo(() => { - const numberOfHeaders = sections.filter(section => section.listItemType === 'header').length; - const numberOfCoinRows = sections.filter(section => section.listItemType === 'coinRow').length; - const totalHeight = numberOfHeaders * BUY_LIST_HEADER_HEIGHT + numberOfCoinRows * COIN_ROW_WITH_PADDING_HEIGHT; - return totalHeight / (numberOfHeaders + numberOfCoinRows); - }, [sections]); + const animatedListProps = useAnimatedProps(() => { + const isFocused = outputProgress.value === 2; + return { + scrollIndicatorInsets: { bottom: 28 + (isFocused ? EXPANDED_INPUT_HEIGHT - FOCUSED_INPUT_HEIGHT : 0) }, + }; + }); if (isLoading) return null; @@ -145,18 +154,14 @@ export const TokenToBuyList = () => { return ( - } ListFooterComponent={} ListHeaderComponent={} contentContainerStyle={{ paddingBottom: 16 }} - // For some reason shallow copying the list data allows FlashList to more quickly pick up changes - data={sections.slice(0)} - estimatedFirstItemOffset={BUY_LIST_HEADER_HEIGHT} - estimatedItemSize={averageItemSize || undefined} - estimatedListSize={{ height: EXPANDED_INPUT_HEIGHT - 77, width: DEVICE_WIDTH - 24 }} - getItemType={item => item.listItemType} + data={sections} + getItemLayout={getItemLayout} keyExtractor={item => `${item.listItemType}-${item.listItemType === 'coinRow' ? item.uniqueId : item.id}`} renderItem={({ item }) => { if (item.listItemType === 'header') { @@ -180,7 +185,15 @@ export const TokenToBuyList = () => { /> ); }} - renderScrollComponent={ScrollViewWithRef as ComponentType} + renderScrollComponent={props => { + return ( + + ); + }} style={{ height: EXPANDED_INPUT_HEIGHT - 77, width: DEVICE_WIDTH - 24 }} /> diff --git a/src/__swaps__/screens/Swap/components/TokenList/TokenToSellList.tsx b/src/__swaps__/screens/Swap/components/TokenList/TokenToSellList.tsx index f28acfd8dfb..38e95e2475a 100644 --- a/src/__swaps__/screens/Swap/components/TokenList/TokenToSellList.tsx +++ b/src/__swaps__/screens/Swap/components/TokenList/TokenToSellList.tsx @@ -1,3 +1,4 @@ +import { FlatList } from 'react-native'; import { COIN_ROW_WITH_PADDING_HEIGHT, CoinRow } from '@/__swaps__/screens/Swap/components/CoinRow'; import { ListEmpty } from '@/__swaps__/screens/Swap/components/TokenList/ListEmpty'; import { useSwapContext } from '@/__swaps__/screens/Swap/providers/swap-provider'; @@ -10,7 +11,6 @@ import * as i18n from '@/languages'; import { userAssetsStore } from '@/state/assets/userAssets'; import { swapsStore } from '@/state/swaps/swapsStore'; import { DEVICE_WIDTH } from '@/utils/deviceUtils'; -import { FlashList } from '@shopify/flash-list'; import React, { useCallback, useMemo } from 'react'; import Animated, { runOnUI, useAnimatedProps, useAnimatedStyle } from 'react-native-reanimated'; import { EXPANDED_INPUT_HEIGHT, FOCUSED_INPUT_HEIGHT } from '../../constants'; @@ -22,6 +22,12 @@ const isInitialInputAssetNull = () => { return !swapsStore.getState().inputAsset; }; +const getItemLayout = (_: ArrayLike | null | undefined, index: number) => ({ + length: COIN_ROW_WITH_PADDING_HEIGHT, + offset: COIN_ROW_WITH_PADDING_HEIGHT * index, + index, +}); + export const TokenToSellList = () => { const skipDelayedMount = useMemo(() => isInitialInputAssetNull(), []); const shouldMount = useDelayedMount({ skipDelayedMount }); @@ -80,16 +86,13 @@ const TokenToSellListComponent = () => { }); return ( - } ListFooterComponent={} ListHeaderComponent={} contentContainerStyle={{ paddingBottom: 16 }} - // For some reason shallow copying the list data allows FlashList to more quickly pick up changes - data={userAssetIds.slice(0)} - estimatedFirstItemOffset={SELL_LIST_HEADER_HEIGHT} - estimatedItemSize={COIN_ROW_WITH_PADDING_HEIGHT} - estimatedListSize={{ height: EXPANDED_INPUT_HEIGHT - 77, width: DEVICE_WIDTH - 24 }} + data={userAssetIds} + getItemLayout={getItemLayout} keyExtractor={uniqueId => uniqueId} renderItem={({ item: uniqueId }) => { return handleSelectToken(asset)} output={false} uniqueId={uniqueId} />; diff --git a/src/__swaps__/screens/Swap/hooks/useAnimatedSwapStyles.ts b/src/__swaps__/screens/Swap/hooks/useAnimatedSwapStyles.ts index b07a99dfdd1..2c2c791ad81 100644 --- a/src/__swaps__/screens/Swap/hooks/useAnimatedSwapStyles.ts +++ b/src/__swaps__/screens/Swap/hooks/useAnimatedSwapStyles.ts @@ -23,7 +23,7 @@ import { safeAreaInsetValues } from '@/utils'; import { getSoftMenuBarHeight } from 'react-native-extra-dimensions-android'; import { DerivedValue, SharedValue, interpolate, useAnimatedStyle, useDerivedValue, withSpring, withTiming } from 'react-native-reanimated'; import { NavigationSteps } from './useSwapNavigation'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; import { SPRING_CONFIGS, TIMING_CONFIGS } from '@/components/animations/animationConfigs'; const INSET_BOTTOM = IS_ANDROID ? getSoftMenuBarHeight() - 24 : safeAreaInsetValues.bottom + 16; diff --git a/src/__swaps__/screens/Swap/hooks/useAssetsToSell.ts b/src/__swaps__/screens/Swap/hooks/useAssetsToSell.ts index a2d181c9a08..53efd2f9cc1 100644 --- a/src/__swaps__/screens/Swap/hooks/useAssetsToSell.ts +++ b/src/__swaps__/screens/Swap/hooks/useAssetsToSell.ts @@ -10,7 +10,7 @@ import { useUserAssets } from '@/__swaps__/screens/Swap/resources/assets'; import { ParsedAssetsDictByChain, ParsedSearchAsset, UserAssetFilter } from '@/__swaps__/types/assets'; import { useAccountSettings, useDebounce } from '@/hooks'; import { userAssetsStore } from '@/state/assets/userAssets'; -import { getIsHardhatConnected } from '@/handlers/web3'; +import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; const sortBy = (by: UserAssetFilter) => { switch (by) { @@ -22,15 +22,15 @@ const sortBy = (by: UserAssetFilter) => { }; export const useAssetsToSell = () => { - const { accountAddress: currentAddress, nativeCurrency: currentCurrency, network: currentNetwork } = useAccountSettings(); - - const connectedToHardhat = getIsHardhatConnected(); + const { accountAddress: currentAddress, nativeCurrency: currentCurrency } = useAccountSettings(); const filter = userAssetsStore(state => state.filter); const searchQuery = userAssetsStore(state => state.inputSearchQuery); const debouncedAssetToSellFilter = useDebounce(searchQuery, 200); + const { connectedToHardhat } = useConnectedToHardhatStore(); + const { data: userAssets = [] } = useUserAssets( { address: currentAddress as Address, diff --git a/src/__swaps__/screens/Swap/hooks/useCustomGas.ts b/src/__swaps__/screens/Swap/hooks/useCustomGas.ts index b6cfa892e40..13aa6388d50 100644 --- a/src/__swaps__/screens/Swap/hooks/useCustomGas.ts +++ b/src/__swaps__/screens/Swap/hooks/useCustomGas.ts @@ -1,4 +1,4 @@ -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; import { createRainbowStore } from '@/state/internal/createRainbowStore'; export type EIP1159GasSettings = { diff --git a/src/__swaps__/screens/Swap/hooks/useEstimatedGasFee.ts b/src/__swaps__/screens/Swap/hooks/useEstimatedGasFee.ts index 88a5b717573..17fb0d613a8 100644 --- a/src/__swaps__/screens/Swap/hooks/useEstimatedGasFee.ts +++ b/src/__swaps__/screens/Swap/hooks/useEstimatedGasFee.ts @@ -1,4 +1,4 @@ -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; import { weiToGwei } from '@/__swaps__/utils/ethereum'; import { add, convertAmountToNativeDisplayWorklet, formatNumber, multiply } from '@/__swaps__/utils/numbers'; import { useNativeAsset } from '@/utils/ethereumUtils'; diff --git a/src/__swaps__/screens/Swap/hooks/useNativeAssetForChain.ts b/src/__swaps__/screens/Swap/hooks/useNativeAssetForChain.ts index 1f03494ed2c..5d124bde586 100644 --- a/src/__swaps__/screens/Swap/hooks/useNativeAssetForChain.ts +++ b/src/__swaps__/screens/Swap/hooks/useNativeAssetForChain.ts @@ -1,7 +1,7 @@ import { useCallback } from 'react'; import { ExtendedAnimatedAssetWithColors } from '@/__swaps__/types/assets'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; import { SharedValue, runOnJS, useAnimatedReaction, useDerivedValue, useSharedValue } from 'react-native-reanimated'; import { ParsedAddressAsset } from '@/entities'; @@ -9,11 +9,11 @@ import { ethereumUtils } from '@/utils'; export const useNativeAssetForChain = ({ inputAsset }: { inputAsset: SharedValue }) => { const chainId = useDerivedValue(() => inputAsset.value?.chainId ?? ChainId.mainnet); - const nativeAsset = useSharedValue(ethereumUtils.getNetworkNativeAsset(chainId.value)); + const nativeAsset = useSharedValue(ethereumUtils.getNetworkNativeAsset({ chainId: chainId.value })); const getNativeAssetForNetwork = useCallback( (chainId: ChainId) => { - const asset = ethereumUtils.getNetworkNativeAsset(chainId); + const asset = ethereumUtils.getNetworkNativeAsset({ chainId }); nativeAsset.value = asset; }, [nativeAsset] @@ -21,11 +21,12 @@ export const useNativeAssetForChain = ({ inputAsset }: { inputAsset: SharedValue useAnimatedReaction( () => chainId.value, - (currentChainId, previoudChainId) => { - if (currentChainId !== previoudChainId) { + (currentChainId, previousChainId) => { + if (currentChainId !== previousChainId) { runOnJS(getNativeAssetForNetwork)(currentChainId); } - } + }, + [] ); return { diff --git a/src/__swaps__/screens/Swap/hooks/useSearchCurrencyLists.ts b/src/__swaps__/screens/Swap/hooks/useSearchCurrencyLists.ts index bb6a5c2ff3b..87815861728 100644 --- a/src/__swaps__/screens/Swap/hooks/useSearchCurrencyLists.ts +++ b/src/__swaps__/screens/Swap/hooks/useSearchCurrencyLists.ts @@ -1,6 +1,6 @@ import { TokenSearchResult, useTokenSearch } from '@/__swaps__/screens/Swap/resources/search/search'; import { AddressOrEth } from '@/__swaps__/types/assets'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; import { SearchAsset, TokenSearchAssetKey, TokenSearchThreshold } from '@/__swaps__/types/search'; import { addHexPrefix } from '@/__swaps__/utils/hex'; import { isLowerCaseMatch } from '@/__swaps__/utils/strings'; @@ -259,23 +259,35 @@ export function useSearchCurrencyLists() { }); // Delays the state set by a frame or two to give animated UI that responds to selectedOutputChainId.value - // a moment to update before the heavy re-renders kicked off by these state changes occur. + // a moment to update before the heavy re-renders kicked off by these state changes occur. This is used + // when the user changes the selected chain in the output token list. const debouncedStateSet = useDebouncedCallback(setState, 20, { leading: false, trailing: true }); + // This is used when the input asset is changed. To avoid a heavy re-render while the input bubble is collapsing, + // we use a longer delay as in this case the list is not visible, so it doesn't need to react immediately. + const changedInputAssetStateSet = useDebouncedCallback(setState, 600, { leading: false, trailing: true }); + useAnimatedReaction( () => ({ isCrosschainSearch: assetToSell.value ? assetToSell.value.chainId !== selectedOutputChainId.value : false, toChainId: selectedOutputChainId.value ?? ChainId.mainnet, }), (current, previous) => { - if (previous && (current.isCrosschainSearch !== previous.isCrosschainSearch || current.toChainId !== previous.toChainId)) { - runOnJS(debouncedStateSet)({ - fromChainId: assetToSell.value ? assetToSell.value.chainId ?? ChainId.mainnet : undefined, - isCrosschainSearch: current.isCrosschainSearch, - toChainId: current.toChainId, - }); - } - } + const toChainIdChanged = previous && current.toChainId !== previous.toChainId; + const isCrosschainSearchChanged = previous && current.isCrosschainSearch !== previous.isCrosschainSearch; + + if (!toChainIdChanged && !isCrosschainSearchChanged) return; + + const newState = { + fromChainId: assetToSell.value ? assetToSell.value.chainId ?? ChainId.mainnet : undefined, + isCrosschainSearch: current.isCrosschainSearch, + toChainId: current.toChainId, + }; + + if (toChainIdChanged) runOnJS(debouncedStateSet)(newState); + else if (isCrosschainSearchChanged) runOnJS(changedInputAssetStateSet)(newState); + }, + [] ); const selectTopSearchResults = useCallback( diff --git a/src/__swaps__/screens/Swap/hooks/useSelectedGas.ts b/src/__swaps__/screens/Swap/hooks/useSelectedGas.ts index 3cf7f91cd48..40f066fec79 100644 --- a/src/__swaps__/screens/Swap/hooks/useSelectedGas.ts +++ b/src/__swaps__/screens/Swap/hooks/useSelectedGas.ts @@ -1,4 +1,4 @@ -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; import { GasSpeed } from '@/__swaps__/types/gas'; import { getCachedGasSuggestions, useMeteorologySuggestions } from '@/__swaps__/utils/meteorology'; import { createRainbowStore } from '@/state/internal/createRainbowStore'; diff --git a/src/__swaps__/screens/Swap/hooks/useSwapEstimatedGasLimit.ts b/src/__swaps__/screens/Swap/hooks/useSwapEstimatedGasLimit.ts index 9149ee472bb..bffed48cf60 100644 --- a/src/__swaps__/screens/Swap/hooks/useSwapEstimatedGasLimit.ts +++ b/src/__swaps__/screens/Swap/hooks/useSwapEstimatedGasLimit.ts @@ -2,7 +2,7 @@ import { CrosschainQuote, Quote, QuoteError, SwapType } from '@rainbow-me/swaps' import { useQuery } from '@tanstack/react-query'; import { ParsedSearchAsset } from '@/__swaps__/types/assets'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; import { estimateUnlockAndCrosschainSwap } from '@/raps/unlockAndCrosschainSwap'; import { estimateUnlockAndSwap } from '@/raps/unlockAndSwap'; import { QueryConfigWithSelect, QueryFunctionArgs, QueryFunctionResult, createQueryKey } from '@/react-query'; diff --git a/src/__swaps__/screens/Swap/hooks/useSwapInputsController.ts b/src/__swaps__/screens/Swap/hooks/useSwapInputsController.ts index 6aa89017e0a..3f07c1ec368 100644 --- a/src/__swaps__/screens/Swap/hooks/useSwapInputsController.ts +++ b/src/__swaps__/screens/Swap/hooks/useSwapInputsController.ts @@ -1,7 +1,7 @@ import { divWorklet, equalWorklet, greaterThanWorklet, isNumberStringWorklet, mulWorklet } from '@/__swaps__/safe-math/SafeMath'; import { SCRUBBER_WIDTH, SLIDER_WIDTH, snappySpringConfig } from '@/__swaps__/screens/Swap/constants'; import { ExtendedAnimatedAssetWithColors } from '@/__swaps__/types/assets'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; import { RequestNewQuoteParams, inputKeys, inputMethods, inputValuesType } from '@/__swaps__/types/swap'; import { valueBasedDecimalFormatter } from '@/__swaps__/utils/decimalFormatter'; import { getInputValuesForSliderPositionWorklet, updateInputValuesAfterFlip } from '@/__swaps__/utils/flipAssets'; @@ -32,6 +32,7 @@ import { useCallback } from 'react'; import { SharedValue, runOnJS, runOnUI, useAnimatedReaction, useDerivedValue, useSharedValue, withSpring } from 'react-native-reanimated'; import { useDebouncedCallback } from 'use-debounce'; import { NavigationSteps } from './useSwapNavigation'; +import { deepEqualWorklet } from '@/worklets/comparisons'; const REMOTE_CONFIG = getRemoteConfig(); @@ -701,7 +702,8 @@ export function useSwapInputsController({ if (areBothAssetsSet) { fetchQuoteAndAssetPrices(); } - } + }, + [] ); /** @@ -722,7 +724,8 @@ export function useSwapInputsController({ }); fetchQuoteAndAssetPrices(); } - } + }, + [] ); /** @@ -752,7 +755,8 @@ export function useSwapInputsController({ } } } - } + }, + [] ); /** @@ -771,11 +775,11 @@ export function useSwapInputsController({ values: inputValues.value, }), (current, previous) => { - if (previous && current !== previous) { + if (previous && !deepEqualWorklet(current, previous)) { // Handle updating input values based on the input method if (inputMethod.value === 'slider' && internalSelectedInputAsset.value && current.sliderXPosition !== previous.sliderXPosition) { // If the slider position changes - if (percentageToSwap.value === 0) { + if (current.sliderXPosition === 0) { resetValuesToZeroWorklet({ updateSlider: false }); } else { // If the change set the slider position to > 0 @@ -870,7 +874,8 @@ export function useSwapInputsController({ } } } - } + }, + [] ); return { debouncedFetchQuote, diff --git a/src/__swaps__/screens/Swap/hooks/useSwapTextStyles.ts b/src/__swaps__/screens/Swap/hooks/useSwapTextStyles.ts index 6404bb425e9..92facea2c77 100644 --- a/src/__swaps__/screens/Swap/hooks/useSwapTextStyles.ts +++ b/src/__swaps__/screens/Swap/hooks/useSwapTextStyles.ts @@ -87,7 +87,10 @@ export function useSwapTextStyles({ }); const isOutputZero = useDerivedValue(() => { - const isZero = !internalSelectedOutputAsset.value || equalWorklet(inputValues.value.outputAmount, 0); + const isZero = + !internalSelectedOutputAsset.value || + (inputValues.value.outputAmount === 0 && inputMethod.value !== 'slider') || + (inputMethod.value === 'slider' && equalWorklet(inputValues.value.outputAmount, 0)); return isZero; }); diff --git a/src/__swaps__/screens/Swap/providers/SyncSwapStateAndSharedValues.tsx b/src/__swaps__/screens/Swap/providers/SyncSwapStateAndSharedValues.tsx index 3cf024f750c..afaff055cd8 100644 --- a/src/__swaps__/screens/Swap/providers/SyncSwapStateAndSharedValues.tsx +++ b/src/__swaps__/screens/Swap/providers/SyncSwapStateAndSharedValues.tsx @@ -1,26 +1,28 @@ +import BigNumber from 'bignumber.js'; import { divWorklet, greaterThanWorklet, + isNumberStringWorklet, lessThanOrEqualToWorklet, lessThanWorklet, mulWorklet, powWorklet, subWorklet, + sumWorklet, toFixedWorklet, toScaledIntegerWorklet, } from '@/__swaps__/safe-math/SafeMath'; import { ExtendedAnimatedAssetWithColors } from '@/__swaps__/types/assets'; -import { ChainId } from '@/__swaps__/types/chains'; -import { add } from '@/__swaps__/utils/numbers'; +import { ChainId } from '@/networks/types'; import { ParsedAddressAsset } from '@/entities'; import { useUserNativeNetworkAsset } from '@/resources/assets/useUserAsset'; import { CrosschainQuote, Quote, QuoteError } from '@rainbow-me/swaps'; +import { deepEqualWorklet } from '@/worklets/comparisons'; import { debounce } from 'lodash'; -import { useEffect } from 'react'; -import { runOnJS, useAnimatedReaction, useSharedValue } from 'react-native-reanimated'; -import { formatUnits } from 'viem'; +import { useEffect, useMemo } from 'react'; +import { runOnJS, runOnUI, useAnimatedReaction, useSharedValue } from 'react-native-reanimated'; import { create } from 'zustand'; -import { calculateGasFee } from '../hooks/useEstimatedGasFee'; +import { GasSettings } from '../hooks/useCustomGas'; import { useSelectedGas } from '../hooks/useSelectedGas'; import { useSwapEstimatedGasLimit } from '../hooks/useSwapEstimatedGasLimit'; import { useSwapContext } from './swap-provider'; @@ -62,7 +64,7 @@ export const SyncQuoteSharedValuesToState = () => { // needed and was previously resulting in errors in useEstimatedGasFee. if (isSwappingMoreThanAvailableBalance) return; - if (!previous || current !== previous) { + if (!deepEqualWorklet(current, previous)) { runOnJS(setInternalSyncedSwapStore)({ assetToBuy: assetToBuy.value, assetToSell: assetToSell.value, @@ -70,18 +72,48 @@ export const SyncQuoteSharedValuesToState = () => { quote: current, }); } - } + }, + [] ); return null; }; -const getHasEnoughFundsForGas = (quote: Quote, gasFee: string, nativeNetworkAsset: ParsedAddressAsset | undefined) => { +export function calculateGasFeeWorklet(gasSettings: GasSettings, gasLimit: string) { + 'worklet'; + const amount = gasSettings.isEIP1559 ? sumWorklet(gasSettings.maxBaseFee, gasSettings.maxPriorityFee || '0') : gasSettings.gasPrice; + return mulWorklet(gasLimit, amount); +} + +export function formatUnitsWorklet(value: string, decimals: number) { + 'worklet'; + let display = value; + const negative = display.startsWith('-'); + if (negative) display = display.slice(1); + + display = display.padStart(decimals, '0'); + + // eslint-disable-next-line prefer-const + let [integer, fraction] = [display.slice(0, display.length - decimals), display.slice(display.length - decimals)]; + fraction = fraction.replace(/(0+)$/, ''); + return `${negative ? '-' : ''}${integer || '0'}${fraction ? `.${fraction}` : ''}`; +} + +const getHasEnoughFundsForGasWorklet = ({ + gasFee, + nativeNetworkAsset, + quoteValue, +}: { + gasFee: string; + nativeNetworkAsset: ParsedAddressAsset | undefined; + quoteValue: string; +}) => { + 'worklet'; if (!nativeNetworkAsset) return false; - const userBalance = nativeNetworkAsset.balance?.amount || '0'; - const quoteValue = quote.value?.toString() || '0'; - const totalNativeSpentInTx = formatUnits(BigInt(add(quoteValue, gasFee)), nativeNetworkAsset.decimals); + const userBalance = nativeNetworkAsset.balance?.amount || '0'; + const safeGasFee = isNumberStringWorklet(gasFee) ? gasFee : '0'; + const totalNativeSpentInTx = formatUnitsWorklet(sumWorklet(quoteValue, safeGasFee), nativeNetworkAsset.decimals); return lessThanOrEqualToWorklet(totalNativeSpentInTx, userBalance); }; @@ -89,7 +121,8 @@ const getHasEnoughFundsForGas = (quote: Quote, gasFee: string, nativeNetworkAsse export function SyncGasStateToSharedValues() { const { hasEnoughFundsForGas, internalSelectedInputAsset } = useSwapContext(); - const { assetToSell, chainId = ChainId.mainnet, quote } = useSyncedSwapQuoteStore(); + const initialChainId = useMemo(() => internalSelectedInputAsset.value?.chainId || ChainId.mainnet, [internalSelectedInputAsset]); + const { assetToSell, chainId = initialChainId, quote } = useSyncedSwapQuoteStore(); const gasSettings = useSelectedGas(chainId); const { data: userNativeNetworkAsset, isLoading: isLoadingNativeNetworkAsset } = useUserNativeNetworkAsset(chainId); @@ -122,46 +155,55 @@ export function SyncGasStateToSharedValues() { }); } } - } + }, + [] ); useEffect(() => { - hasEnoughFundsForGas.value = undefined; - if (!gasSettings || !estimatedGasLimit || !quote || 'error' in quote || isLoadingNativeNetworkAsset) return; - - // NOTE: if we don't have a gas price or max base fee or max priority fee, we can't calculate the gas fee - if ( - (gasSettings.isEIP1559 && !(gasSettings.maxBaseFee || gasSettings.maxPriorityFee)) || - (!gasSettings.isEIP1559 && !gasSettings.gasPrice) - ) { - return; - } - - if (!userNativeNetworkAsset) { - hasEnoughFundsForGas.value = false; - return; - } - - const gasFee = calculateGasFee(gasSettings, estimatedGasLimit); - if (gasFee === null || isNaN(Number(gasFee))) { - return; - } - - const nativeGasFee = divWorklet(gasFee, powWorklet(10, userNativeNetworkAsset.decimals)); - - const isEstimateOutsideRange = !!( - gasFeeRange.value && - (lessThanWorklet(nativeGasFee, gasFeeRange.value[0]) || greaterThanWorklet(nativeGasFee, gasFeeRange.value[1])) - ); - - // If the gas fee range hasn't been set or the estimated fee is outside the range, calculate the range based on the gas fee - if (nativeGasFee && (!gasFeeRange.value || isEstimateOutsideRange)) { - const lowerBound = toFixedWorklet(mulWorklet(nativeGasFee, 1 - BUFFER_RATIO), userNativeNetworkAsset.decimals); - const upperBound = toFixedWorklet(mulWorklet(nativeGasFee, 1 + BUFFER_RATIO), userNativeNetworkAsset.decimals); - gasFeeRange.value = [lowerBound, upperBound]; - } - - hasEnoughFundsForGas.value = getHasEnoughFundsForGas(quote, gasFee, userNativeNetworkAsset); + const safeQuoteValue = quote && !('error' in quote) && quote.value ? new BigNumber(quote.value.toString()).toFixed() : '0'; + + runOnUI(() => { + hasEnoughFundsForGas.value = undefined; + if (!gasSettings || !estimatedGasLimit || !quote || 'error' in quote || isLoadingNativeNetworkAsset) return; + + // NOTE: if we don't have a gas price or max base fee or max priority fee, we can't calculate the gas fee + if ( + (gasSettings.isEIP1559 && !(gasSettings.maxBaseFee || gasSettings.maxPriorityFee)) || + (!gasSettings.isEIP1559 && !gasSettings.gasPrice) + ) { + return; + } + + if (!userNativeNetworkAsset) { + hasEnoughFundsForGas.value = false; + return; + } + + const gasFee = calculateGasFeeWorklet(gasSettings, estimatedGasLimit); + if (gasFee === null || isNaN(Number(gasFee))) { + return; + } + + const nativeGasFee = divWorklet(gasFee, powWorklet(10, userNativeNetworkAsset.decimals)); + + const isEstimateOutsideRange = !!( + gasFeeRange.value && + (lessThanWorklet(nativeGasFee, gasFeeRange.value[0]) || greaterThanWorklet(nativeGasFee, gasFeeRange.value[1])) + ); + + // If the gas fee range hasn't been set or the estimated fee is outside the range, calculate the range based on the gas fee + if (nativeGasFee && (!gasFeeRange.value || isEstimateOutsideRange)) { + const lowerBound = toFixedWorklet(mulWorklet(nativeGasFee, 1 - BUFFER_RATIO), userNativeNetworkAsset.decimals); + const upperBound = toFixedWorklet(mulWorklet(nativeGasFee, 1 + BUFFER_RATIO), userNativeNetworkAsset.decimals); + gasFeeRange.value = [lowerBound, upperBound]; + } + + hasEnoughFundsForGas.value = getHasEnoughFundsForGasWorklet({ + gasFee, + nativeNetworkAsset: userNativeNetworkAsset, + quoteValue: safeQuoteValue, + }); + })(); return () => { hasEnoughFundsForGas.value = undefined; @@ -171,6 +213,7 @@ export function SyncGasStateToSharedValues() { gasFeeRange, gasSettings, hasEnoughFundsForGas, + internalSelectedInputAsset, quote, userNativeNetworkAsset, isLoadingNativeNetworkAsset, diff --git a/src/__swaps__/screens/Swap/providers/swap-provider.tsx b/src/__swaps__/screens/Swap/providers/swap-provider.tsx index 5ae640688a2..4740f3b61c0 100644 --- a/src/__swaps__/screens/Swap/providers/swap-provider.tsx +++ b/src/__swaps__/screens/Swap/providers/swap-provider.tsx @@ -24,12 +24,12 @@ import { useSwapTextStyles } from '@/__swaps__/screens/Swap/hooks/useSwapTextSty import { SwapWarningType, useSwapWarning } from '@/__swaps__/screens/Swap/hooks/useSwapWarning'; import { userAssetsQueryKey as swapsUserAssetsQueryKey } from '@/__swaps__/screens/Swap/resources/assets/userAssets'; import { AddressOrEth, ExtendedAnimatedAssetWithColors, ParsedSearchAsset } from '@/__swaps__/types/assets'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; import { SwapAssetType, inputKeys } from '@/__swaps__/types/swap'; import { getDefaultSlippageWorklet, isUnwrapEthWorklet, isWrapEthWorklet, parseAssetAndExtend } from '@/__swaps__/utils/swaps'; import { analyticsV2 } from '@/analytics'; import { LegacyTransactionGasParamAmounts, TransactionGasParamAmounts } from '@/entities'; -import { getFlashbotsProvider, getIsHardhatConnected, getProvider, isHardHat } from '@/handlers/web3'; +import { getFlashbotsProvider, getProvider } from '@/handlers/web3'; import { WrappedAlert as Alert } from '@/helpers/alert'; import { useAccountSettings } from '@/hooks'; import { useAnimatedInterval } from '@/hooks/reanimated/useAnimatedInterval'; @@ -56,9 +56,11 @@ import { useSwapOutputQuotesDisabled } from '../hooks/useSwapOutputQuotesDisable import { SyncGasStateToSharedValues, SyncQuoteSharedValuesToState } from './SyncSwapStateAndSharedValues'; import { performanceTracking, Screens, TimeToSignOperation } from '@/state/performance/performance'; import { getRemoteConfig } from '@/model/remoteConfig'; +import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; const swapping = i18n.t(i18n.l.swap.actions.swapping); const holdToSwap = i18n.t(i18n.l.swap.actions.hold_to_swap); +const holdToBridge = i18n.t(i18n.l.swap.actions.hold_to_bridge); const done = i18n.t(i18n.l.button.done); const enterAmount = i18n.t(i18n.l.swap.actions.enter_amount); const review = i18n.t(i18n.l.swap.actions.review); @@ -164,6 +166,8 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { const slippage = useSharedValue(getDefaultSlippageWorklet(initialSelectedInputAsset?.chainId || ChainId.mainnet, getRemoteConfig())); + const hasEnoughFundsForGas = useSharedValue(undefined); + const SwapInputController = useSwapInputsController({ focusedInput, lastTypedInput, @@ -199,8 +203,7 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { parameters.flashbots && getNetworkObject({ chainId: parameters.chainId }).features.flashbots ? await getFlashbotsProvider() : getProvider({ chainId: parameters.chainId }); - const providerUrl = provider?.connection?.url; - const connectedToHardhat = !!providerUrl && isHardHat(providerUrl); + const connectedToHardhat = useConnectedToHardhatStore.getState().connectedToHardhat; const isBridge = swapsStore.getState().inputAsset?.mainnetAddress === swapsStore.getState().outputAsset?.mainnetAddress; const isDegenModeEnabled = swapsStore.getState().degenMode; @@ -258,7 +261,7 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { }; } - const chainId = getIsHardhatConnected() ? ChainId.hardhat : parameters.chainId; + const chainId = connectedToHardhat ? ChainId.hardhat : parameters.chainId; const { errorMessage } = await performanceTracking.getState().executeFn({ fn: walletExecuteRap, screen: Screens.SWAPS, @@ -664,12 +667,18 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { }; }, []); - const hasEnoughFundsForGas = useSharedValue(undefined); + // Stop auto-fetching if there is a quote error or no input asset balance useAnimatedReaction( - () => isFetching.value, - fetching => { - if (fetching) hasEnoughFundsForGas.value = undefined; - } + () => + SwapWarning.swapWarning.value.type === SwapWarningType.no_quote_available || + SwapWarning.swapWarning.value.type === SwapWarningType.no_route_found || + (internalSelectedInputAsset.value && equalWorklet(internalSelectedInputAsset.value.maxSwappableAmount, '0')), + (shouldStop, previous) => { + if (shouldStop && previous === false) { + SwapInputController.quoteFetchingInterval.stop(); + } + }, + [] ); const confirmButtonProps: SwapContextType['confirmButtonProps'] = useDerivedValue(() => { @@ -690,51 +699,55 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { return { label: selectToken, disabled: true, type: 'hold' }; } + const sellAsset = internalSelectedInputAsset.value; + const enoughFundsForSwap = + sellAsset && + !equalWorklet(sellAsset.maxSwappableAmount, '0') && + lessThanOrEqualToWorklet(SwapInputController.inputValues.value.inputAmount, sellAsset.maxSwappableAmount); + + if (!enoughFundsForSwap && hasEnoughFundsForGas.value !== undefined) { + return { label: insufficientFunds, disabled: true, type: 'hold' }; + } + const isInputZero = equalWorklet(SwapInputController.inputValues.value.inputAmount, 0); const isOutputZero = equalWorklet(SwapInputController.inputValues.value.outputAmount, 0); const userHasNotEnteredAmount = SwapInputController.inputMethod.value !== 'slider' && isInputZero && isOutputZero; - const userHasNotMovedSlider = SwapInputController.inputMethod.value === 'slider' && SwapInputController.percentageToSwap.value === 0; if (userHasNotEnteredAmount || userHasNotMovedSlider) { return { label: enterAmount, disabled: true, opacity: 1, type: 'hold' }; } - if ( - [SwapWarningType.no_quote_available, SwapWarningType.no_route_found, SwapWarningType.insufficient_liquidity].includes( - SwapWarning.swapWarning.value.type - ) - ) { - return { icon: '􀕹', label: review, disabled: true, type: 'hold' }; - } - - const sellAsset = internalSelectedInputAsset.value; - const enoughFundsForSwap = - sellAsset && lessThanOrEqualToWorklet(SwapInputController.inputValues.value.inputAmount, sellAsset.maxSwappableAmount); - - if (!enoughFundsForSwap) { - return { label: insufficientFunds, disabled: true, type: 'hold' }; - } + const holdLabel = swapInfo.value.isBridging ? holdToBridge : holdToSwap; + const reviewLabel = SwapSettings.degenMode.value ? holdLabel : review; const isQuoteError = quote.value && 'error' in quote.value; const isLoadingGas = !isQuoteError && hasEnoughFundsForGas.value === undefined; const isReviewSheetOpen = configProgress.value === NavigationSteps.SHOW_REVIEW || SwapSettings.degenMode.value; - if ((isFetching.value || isLoadingGas) && !isQuoteError) { - const disabled = (isReviewSheetOpen && (isFetching.value || isLoadingGas)) || !quote.value; + const isStale = + !!isQuoteStale.value && + (SwapInputController.inputMethod.value !== 'slider' || sliderPressProgress.value === SLIDER_COLLAPSED_HEIGHT / SLIDER_HEIGHT); + + if ((isFetching.value || isLoadingGas || isStale) && !isQuoteError) { + const disabled = (isReviewSheetOpen && (isFetching.value || isLoadingGas || isStale)) || !quote.value; const buttonType = isReviewSheetOpen ? 'hold' : 'tap'; return { label: fetchingPrices, disabled, type: buttonType }; } - const reviewLabel = SwapSettings.degenMode.value ? holdToSwap : review; + const quoteUnavailable = [ + SwapWarningType.no_quote_available, + SwapWarningType.no_route_found, + SwapWarningType.insufficient_liquidity, + ].includes(SwapWarning.swapWarning.value.type); - if (isQuoteError) { + if (quoteUnavailable || isQuoteError) { const icon = isReviewSheetOpen ? undefined : '􀕹'; return { icon, label: isReviewSheetOpen ? quoteError : reviewLabel, disabled: true, type: 'hold' }; } - if (!hasEnoughFundsForGas.value) { + if (hasEnoughFundsForGas.value === false) { const nativeCurrency = RainbowNetworkByChainId[sellAsset?.chainId || ChainId.mainnet].nativeCurrency; return { label: `${insufficient} ${nativeCurrency.symbol}`, @@ -744,23 +757,15 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { } if (isReviewSheetOpen) { - return { icon: '􀎽', label: holdToSwap, disabled: false, type: 'hold' }; + const isDraggingSlider = !!isQuoteStale.value && sliderPressProgress.value !== SLIDER_COLLAPSED_HEIGHT / SLIDER_HEIGHT; + return { icon: '􀎽', label: holdLabel, disabled: isDraggingSlider, type: 'hold' }; } return { icon: '􀕹', label: reviewLabel, disabled: false, type: 'tap' }; }); const confirmButtonIconStyle = useAnimatedStyle(() => { - const isInputZero = equalWorklet(SwapInputController.inputValues.value.inputAmount, 0); - const isOutputZero = equalWorklet(SwapInputController.inputValues.value.outputAmount, 0); - - const sliderCondition = - SwapInputController.inputMethod.value === 'slider' && - (SwapInputController.percentageToSwap.value === 0 || isInputZero || isOutputZero); - const inputCondition = SwapInputController.inputMethod.value !== 'slider' && (isInputZero || isOutputZero) && !isFetching.value; - - const shouldHide = sliderCondition || inputCondition; - + const shouldHide = !confirmButtonProps.value.icon; return { display: shouldHide ? 'none' : 'flex', }; diff --git a/src/__swaps__/screens/Swap/resources/_selectors/assets.ts b/src/__swaps__/screens/Swap/resources/_selectors/assets.ts index 9e2e29c05cf..f8c829cc06b 100644 --- a/src/__swaps__/screens/Swap/resources/_selectors/assets.ts +++ b/src/__swaps__/screens/Swap/resources/_selectors/assets.ts @@ -1,5 +1,5 @@ import { ParsedAssetsDict, ParsedAssetsDictByChain, ParsedUserAsset, UniqueId } from '@/__swaps__/types/assets'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; import { deriveAddressAndChainWithUniqueId } from '@/__swaps__/utils/address'; import { add } from '@/__swaps__/utils/numbers'; diff --git a/src/__swaps__/screens/Swap/resources/assets/assets.ts b/src/__swaps__/screens/Swap/resources/assets/assets.ts index 070c11577ee..85d800e5be5 100644 --- a/src/__swaps__/screens/Swap/resources/assets/assets.ts +++ b/src/__swaps__/screens/Swap/resources/assets/assets.ts @@ -4,7 +4,7 @@ import { requestMetadata } from '@/graphql'; import { QueryConfigWithSelect, QueryFunctionArgs, QueryFunctionResult, createQueryKey, queryClient } from '@/react-query'; import { SupportedCurrencyKey } from '@/references'; import { AddressOrEth, AssetMetadata, ParsedAsset, UniqueId } from '@/__swaps__/types/assets'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; import { chunkArray, createAssetQuery, parseAssetMetadata } from '@/__swaps__/utils/assets'; import { RainbowError, logger } from '@/logger'; export const ASSETS_TIMEOUT_DURATION = 10000; diff --git a/src/__swaps__/screens/Swap/resources/assets/userAssets.ts b/src/__swaps__/screens/Swap/resources/assets/userAssets.ts index d6d689682e2..37dc8c9f185 100644 --- a/src/__swaps__/screens/Swap/resources/assets/userAssets.ts +++ b/src/__swaps__/screens/Swap/resources/assets/userAssets.ts @@ -4,12 +4,11 @@ import { Address } from 'viem'; import { QueryConfigWithSelect, QueryFunctionArgs, QueryFunctionResult, createQueryKey, queryClient } from '@/react-query'; -import { getIsHardhatConnected } from '@/handlers/web3'; import { RainbowError, logger } from '@/logger'; import { RainbowFetchClient } from '@/rainbow-fetch'; import { SupportedCurrencyKey, SUPPORTED_CHAIN_IDS } from '@/references'; import { ParsedAssetsDictByChain, ZerionAsset } from '@/__swaps__/types/assets'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; import { AddressAssetsReceivedMessage } from '@/__swaps__/types/refraction'; import { parseUserAsset } from '@/__swaps__/utils/assets'; import { greaterThan } from '@/__swaps__/utils/numbers'; diff --git a/src/__swaps__/screens/Swap/resources/assets/userAssetsByChain.ts b/src/__swaps__/screens/Swap/resources/assets/userAssetsByChain.ts index cb5a12e1e45..b2b130aea98 100644 --- a/src/__swaps__/screens/Swap/resources/assets/userAssetsByChain.ts +++ b/src/__swaps__/screens/Swap/resources/assets/userAssetsByChain.ts @@ -4,7 +4,7 @@ import { Address } from 'viem'; import { QueryConfigWithSelect, QueryFunctionArgs, QueryFunctionResult, createQueryKey, queryClient } from '@/react-query'; import { SupportedCurrencyKey } from '@/references'; import { ParsedAssetsDictByChain, ParsedUserAsset } from '@/__swaps__/types/assets'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; import { AddressAssetsReceivedMessage } from '@/__swaps__/types/refraction'; import { RainbowError, logger } from '@/logger'; diff --git a/src/__swaps__/screens/Swap/resources/search/discovery.ts b/src/__swaps__/screens/Swap/resources/search/discovery.ts index f3b0a9ae468..269f44441b9 100644 --- a/src/__swaps__/screens/Swap/resources/search/discovery.ts +++ b/src/__swaps__/screens/Swap/resources/search/discovery.ts @@ -1,4 +1,4 @@ -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; import { SearchAsset } from '@/__swaps__/types/search'; import { RainbowError, logger } from '@/logger'; import { RainbowFetchClient } from '@/rainbow-fetch'; diff --git a/src/__swaps__/screens/Swap/resources/search/search.ts b/src/__swaps__/screens/Swap/resources/search/search.ts index 9ac2e5b678e..8f29f7eb1d9 100644 --- a/src/__swaps__/screens/Swap/resources/search/search.ts +++ b/src/__swaps__/screens/Swap/resources/search/search.ts @@ -1,5 +1,5 @@ /* eslint-disable no-nested-ternary */ -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; import { SearchAsset, TokenSearchAssetKey, TokenSearchListId, TokenSearchThreshold } from '@/__swaps__/types/search'; import { RainbowError, logger } from '@/logger'; import { RainbowFetchClient } from '@/rainbow-fetch'; diff --git a/src/__swaps__/screens/Swap/resources/search/utils.ts b/src/__swaps__/screens/Swap/resources/search/utils.ts index 0ed74aeb80e..4c649d04cae 100644 --- a/src/__swaps__/screens/Swap/resources/search/utils.ts +++ b/src/__swaps__/screens/Swap/resources/search/utils.ts @@ -1,4 +1,4 @@ -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; import { SearchAsset } from '@/__swaps__/types/search'; import { ARBITRUM_ETH_ADDRESS, diff --git a/src/__swaps__/types/assets.ts b/src/__swaps__/types/assets.ts index 05f78261ff1..73946574888 100644 --- a/src/__swaps__/types/assets.ts +++ b/src/__swaps__/types/assets.ts @@ -1,7 +1,7 @@ import type { Address } from 'viem'; import { ETH_ADDRESS } from '@/references'; -import { ChainId, ChainName } from '@/__swaps__/types/chains'; +import { ChainId, ChainName } from '@/networks/types'; import { SearchAsset } from '@/__swaps__/types/search'; import { ResponseByTheme } from '../utils/swaps'; diff --git a/src/__swaps__/types/chains.ts b/src/__swaps__/types/chains.ts deleted file mode 100644 index ff59ca43b05..00000000000 --- a/src/__swaps__/types/chains.ts +++ /dev/null @@ -1,215 +0,0 @@ -import * as chain from 'viem/chains'; -import type { Chain } from 'viem/chains'; - -const HARDHAT_CHAIN_ID = 1337; -const HARDHAT_OP_CHAIN_ID = 1338; - -export const chainHardhat: Chain = { - id: HARDHAT_CHAIN_ID, - name: 'Hardhat', - nativeCurrency: { - decimals: 18, - name: 'Hardhat', - symbol: 'ETH', - }, - rpcUrls: { - public: { http: ['http://127.0.0.1:8545'] }, - default: { http: ['http://127.0.0.1:8545'] }, - }, - testnet: true, -}; - -export const chainHardhatOptimism: Chain = { - id: HARDHAT_OP_CHAIN_ID, - name: 'Hardhat OP', - nativeCurrency: { - decimals: 18, - name: 'Hardhat OP', - symbol: 'ETH', - }, - rpcUrls: { - public: { http: ['http://127.0.0.1:8545'] }, - default: { http: ['http://127.0.0.1:8545'] }, - }, - testnet: true, -}; - -export enum ChainName { - arbitrum = 'arbitrum', - arbitrumNova = 'arbitrum-nova', - arbitrumSepolia = 'arbitrum-sepolia', - avalanche = 'avalanche', - avalancheFuji = 'avalanche-fuji', - base = 'base', - blast = 'blast', - blastSepolia = 'blast-sepolia', - bsc = 'bsc', - celo = 'celo', - degen = 'degen', - gnosis = 'gnosis', - goerli = 'goerli', - linea = 'linea', - manta = 'manta', - optimism = 'optimism', - polygon = 'polygon', - polygonZkEvm = 'polygon-zkevm', - rari = 'rari', - scroll = 'scroll', - zora = 'zora', - mainnet = 'mainnet', - holesky = 'holesky', - hardhat = 'hardhat', - hardhatOptimism = 'hardhat-optimism', - sepolia = 'sepolia', - optimismSepolia = 'optimism-sepolia', - bscTestnet = 'bsc-testnet', - polygonMumbai = 'polygon-mumbai', - baseSepolia = 'base-sepolia', - zoraSepolia = 'zora-sepolia', - polygonAmoy = 'polygon-amoy', -} - -export enum ChainId { - arbitrum = chain.arbitrum.id, - arbitrumNova = chain.arbitrumNova.id, - arbitrumSepolia = chain.arbitrumSepolia.id, - avalanche = chain.avalanche.id, - avalancheFuji = chain.avalancheFuji.id, - base = chain.base.id, - baseSepolia = chain.baseSepolia.id, - blast = chain.blast.id, - blastSepolia = chain.blastSepolia.id, - bsc = chain.bsc.id, - bscTestnet = chain.bscTestnet.id, - celo = chain.celo.id, - degen = chain.degen.id, - gnosis = chain.gnosis.id, - goerli = chain.goerli.id, - hardhat = HARDHAT_CHAIN_ID, - hardhatOptimism = chainHardhatOptimism.id, - holesky = chain.holesky.id, - linea = chain.linea.id, - mainnet = chain.mainnet.id, - manta = chain.manta.id, - optimism = chain.optimism.id, - optimismSepolia = chain.optimismSepolia.id, - polygon = chain.polygon.id, - polygonAmoy = chain.polygonAmoy.id, - polygonMumbai = chain.polygonMumbai.id, - polygonZkEvm = chain.polygonZkEvm.id, - rari = 1380012617, - scroll = chain.scroll.id, - sepolia = chain.sepolia.id, - zora = chain.zora.id, - zoraSepolia = chain.zoraSepolia.id, -} - -export const chainNameToIdMapping: { - [key in ChainName | 'ethereum' | 'ethereum-sepolia']: ChainId; -} = { - ['ethereum']: ChainId.mainnet, - [ChainName.arbitrum]: ChainId.arbitrum, - [ChainName.arbitrumNova]: ChainId.arbitrumNova, - [ChainName.arbitrumSepolia]: ChainId.arbitrumSepolia, - [ChainName.avalanche]: ChainId.avalanche, - [ChainName.avalancheFuji]: ChainId.avalancheFuji, - [ChainName.base]: ChainId.base, - [ChainName.bsc]: ChainId.bsc, - [ChainName.celo]: ChainId.celo, - [ChainName.degen]: ChainId.degen, - [ChainName.gnosis]: ChainId.gnosis, - [ChainName.goerli]: chain.goerli.id, - [ChainName.linea]: ChainId.linea, - [ChainName.manta]: ChainId.manta, - [ChainName.optimism]: ChainId.optimism, - [ChainName.polygon]: ChainId.polygon, - [ChainName.polygonZkEvm]: ChainId.polygonZkEvm, - [ChainName.rari]: ChainId.rari, - [ChainName.scroll]: ChainId.scroll, - [ChainName.zora]: ChainId.zora, - [ChainName.mainnet]: ChainId.mainnet, - [ChainName.holesky]: ChainId.holesky, - [ChainName.hardhat]: ChainId.hardhat, - [ChainName.hardhatOptimism]: ChainId.hardhatOptimism, - ['ethereum-sepolia']: ChainId.sepolia, - [ChainName.sepolia]: ChainId.sepolia, - [ChainName.optimismSepolia]: ChainId.optimismSepolia, - [ChainName.bscTestnet]: ChainId.bscTestnet, - [ChainName.polygonMumbai]: ChainId.polygonMumbai, - [ChainName.baseSepolia]: ChainId.baseSepolia, - [ChainName.zoraSepolia]: ChainId.zoraSepolia, - [ChainName.blast]: ChainId.blast, - [ChainName.blastSepolia]: ChainId.blastSepolia, - [ChainName.polygonAmoy]: ChainId.polygonAmoy, -}; - -export const chainIdToNameMapping: { - [key in ChainId]: ChainName; -} = { - [ChainId.arbitrum]: ChainName.arbitrum, - [ChainId.arbitrumNova]: ChainName.arbitrumNova, - [ChainId.arbitrumSepolia]: ChainName.arbitrumSepolia, - [ChainId.avalanche]: ChainName.avalanche, - [ChainId.avalancheFuji]: ChainName.avalancheFuji, - [ChainId.base]: ChainName.base, - [ChainId.blast]: ChainName.blast, - [ChainId.blastSepolia]: ChainName.blastSepolia, - [ChainId.bsc]: ChainName.bsc, - [ChainId.celo]: ChainName.celo, - [ChainId.degen]: ChainName.degen, - [ChainId.gnosis]: ChainName.gnosis, - [chain.goerli.id]: ChainName.goerli, - [ChainId.linea]: ChainName.linea, - [ChainId.manta]: ChainName.manta, - [ChainId.optimism]: ChainName.optimism, - [ChainId.polygon]: ChainName.polygon, - [ChainId.polygonZkEvm]: ChainName.polygonZkEvm, - [ChainId.rari]: ChainName.rari, - [ChainId.scroll]: ChainName.scroll, - [ChainId.zora]: ChainName.zora, - [ChainId.mainnet]: ChainName.mainnet, - [ChainId.holesky]: ChainName.holesky, - [ChainId.hardhat]: ChainName.hardhat, - [ChainId.hardhatOptimism]: ChainName.hardhatOptimism, - [ChainId.sepolia]: ChainName.sepolia, - [ChainId.optimismSepolia]: ChainName.optimismSepolia, - [ChainId.bscTestnet]: ChainName.bscTestnet, - [ChainId.polygonMumbai]: ChainName.polygonMumbai, - [ChainId.baseSepolia]: ChainName.baseSepolia, - [ChainId.zoraSepolia]: ChainName.zoraSepolia, - [ChainId.polygonAmoy]: ChainName.polygonAmoy, -}; - -export const ChainNameDisplay = { - [ChainId.arbitrum]: 'Arbitrum', - [ChainId.arbitrumNova]: chain.arbitrumNova.name, - [ChainId.avalanche]: 'Avalanche', - [ChainId.avalancheFuji]: 'Avalanche Fuji', - [ChainId.base]: 'Base', - [ChainId.blast]: 'Blast', - [ChainId.blastSepolia]: 'Blast Sepolia', - [ChainId.bsc]: 'BSC', - [ChainId.celo]: chain.celo.name, - [ChainId.degen]: 'Degen Chain', - [ChainId.linea]: 'Linea', - [ChainId.manta]: 'Manta', - [ChainId.optimism]: 'Optimism', - [ChainId.polygon]: 'Polygon', - [ChainId.polygonZkEvm]: chain.polygonZkEvm.name, - [ChainId.rari]: 'RARI Chain', - [ChainId.scroll]: chain.scroll.name, - [ChainId.zora]: 'Zora', - [ChainId.mainnet]: 'Ethereum', - [chain.goerli.id]: 'Goerli', - [ChainId.hardhat]: 'Hardhat', - [ChainId.hardhatOptimism]: chainHardhatOptimism.name, - [ChainId.sepolia]: chain.sepolia.name, - [ChainId.holesky]: chain.holesky.name, - [ChainId.optimismSepolia]: chain.optimismSepolia.name, - [ChainId.bscTestnet]: 'BSC Testnet', - [ChainId.polygonMumbai]: chain.polygonMumbai.name, - [ChainId.arbitrumSepolia]: chain.arbitrumSepolia.name, - [ChainId.baseSepolia]: chain.baseSepolia.name, - [ChainId.zoraSepolia]: 'Zora Sepolia', - [ChainId.polygonAmoy]: 'Polygon Amoy', -} as const; diff --git a/src/__swaps__/types/refraction.ts b/src/__swaps__/types/refraction.ts index e18be684eb0..9afc8ce9cf3 100644 --- a/src/__swaps__/types/refraction.ts +++ b/src/__swaps__/types/refraction.ts @@ -1,5 +1,5 @@ import { ZerionAsset } from '@/__swaps__/types/assets'; -import { ChainId, ChainName } from '@/__swaps__/types/chains'; +import { ChainId, ChainName } from '@/networks/types'; import { PaginatedTransactionsApiResponse } from '@/resources/transactions/types'; /** diff --git a/src/__swaps__/types/search.ts b/src/__swaps__/types/search.ts index 5acf427de4e..9e0aff40e3d 100644 --- a/src/__swaps__/types/search.ts +++ b/src/__swaps__/types/search.ts @@ -1,7 +1,7 @@ import { Address } from 'viem'; import { AddressOrEth, AssetType, ParsedAsset, UniqueId } from '@/__swaps__/types/assets'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; import { AssetToBuySectionId } from '../screens/Swap/hooks/useSearchCurrencyLists'; export type TokenSearchAssetKey = keyof ParsedAsset; diff --git a/src/__swaps__/utils/address.ts b/src/__swaps__/utils/address.ts index 39fd20091c1..1b56b2ba57e 100644 --- a/src/__swaps__/utils/address.ts +++ b/src/__swaps__/utils/address.ts @@ -1,7 +1,7 @@ import { Address } from 'viem'; import { AddressOrEth, UniqueId } from '@/__swaps__/types/assets'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; export function truncateAddress(address?: AddressOrEth) { if (!address) return ''; diff --git a/src/__swaps__/utils/assets.ts b/src/__swaps__/utils/assets.ts index 8b888c36fc2..420aee6a9b8 100644 --- a/src/__swaps__/utils/assets.ts +++ b/src/__swaps__/utils/assets.ts @@ -1,5 +1,4 @@ import { AddressZero } from '@ethersproject/constants'; -import isValidDomain from 'is-valid-domain'; import { ETH_ADDRESS, SupportedCurrencyKey } from '@/references'; import { @@ -13,7 +12,7 @@ import { ZerionAsset, ZerionAssetPrice, } from '@/__swaps__/types/assets'; -import { ChainId, ChainName } from '@/__swaps__/types/chains'; +import { ChainId, ChainName } from '@/networks/types'; import * as i18n from '@/languages'; import { SearchAsset } from '@/__swaps__/types/search'; diff --git a/src/__swaps__/utils/chains.ts b/src/__swaps__/utils/chains.ts index 1b9ca2bd496..63f821fc1b7 100644 --- a/src/__swaps__/utils/chains.ts +++ b/src/__swaps__/utils/chains.ts @@ -1,7 +1,7 @@ import { celo, fantom, harmonyOne, moonbeam } from 'viem/chains'; import { NATIVE_ASSETS_PER_CHAIN } from '@/references'; import { AddressOrEth } from '@/__swaps__/types/assets'; -import { ChainId, ChainName, ChainNameDisplay, chainIdToNameMapping, chainNameToIdMapping } from '@/__swaps__/types/chains'; +import { ChainId, ChainName, ChainNameDisplay, chainIdToNameMapping, chainNameToIdMapping } from '@/networks/types'; import { isLowerCaseMatch } from '@/__swaps__/utils/strings'; import { getNetworkFromChainId } from '@/utils/ethereumUtils'; diff --git a/src/__swaps__/utils/decimalFormatter.ts b/src/__swaps__/utils/decimalFormatter.ts index 5b047b8c677..fc0f9a943ec 100644 --- a/src/__swaps__/utils/decimalFormatter.ts +++ b/src/__swaps__/utils/decimalFormatter.ts @@ -82,7 +82,7 @@ export function valueBasedDecimalFormatter({ // Format the number to add separators and trim trailing zeros const numberFormatter = new Intl.NumberFormat('en-US', { minimumFractionDigits: minimumDecimalPlaces, - maximumFractionDigits: maximumDecimalPlaces, + maximumFractionDigits: maximumDecimalPlaces || 0, useGrouping: !stripSeparators, }); diff --git a/src/__swaps__/utils/gasUtils.ts b/src/__swaps__/utils/gasUtils.ts index 7e4ff881a7b..5039a93eef1 100644 --- a/src/__swaps__/utils/gasUtils.ts +++ b/src/__swaps__/utils/gasUtils.ts @@ -12,7 +12,7 @@ import { OVM_GAS_PRICE_ORACLE, gasUnits, supportedNativeCurrencies, optimismGasO import { MeteorologyLegacyResponse, MeteorologyResponse } from '@/__swaps__/utils/meteorology'; import { ParsedAsset } from '@/__swaps__/types/assets'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; import { BlocksToConfirmation, GasFeeLegacyParams, GasFeeParam, GasFeeParams, GasSpeed } from '@/__swaps__/types/gas'; import { gweiToWei, weiToGwei } from '@/__swaps__/utils/ethereum'; diff --git a/src/__swaps__/utils/meteorology.ts b/src/__swaps__/utils/meteorology.ts index de0740f4069..c12164ef6d4 100644 --- a/src/__swaps__/utils/meteorology.ts +++ b/src/__swaps__/utils/meteorology.ts @@ -1,12 +1,11 @@ import { useQuery } from '@tanstack/react-query'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; import { rainbowMeteorologyGetData } from '@/handlers/gasFees'; import { abs, lessThan, subtract } from '@/helpers/utilities'; import { gweiToWei } from '@/parsers'; import { QueryConfig, QueryFunctionArgs, QueryFunctionResult, createQueryKey, queryClient } from '@/react-query'; import { useSwapsStore } from '@/state/swaps/swapsStore'; -import { getNetworkFromChainId } from '@/utils/ethereumUtils'; import { useCallback } from 'react'; import { MIN_FLASHBOTS_PRIORITY_FEE } from '../screens/Swap/constants'; import { GasSettings } from '../screens/Swap/hooks/useCustomGas'; @@ -72,8 +71,7 @@ type MeteorologyQueryKey = ReturnType; // Query Function async function meteorologyQueryFunction({ queryKey: [{ chainId }] }: QueryFunctionArgs) { - const network = getNetworkFromChainId(chainId); - const parsedResponse = await rainbowMeteorologyGetData(network); + const parsedResponse = await rainbowMeteorologyGetData(chainId); const meteorologyData = parsedResponse.data as MeteorologyResponse | MeteorologyLegacyResponse; return meteorologyData; } diff --git a/src/__swaps__/utils/swaps.ts b/src/__swaps__/utils/swaps.ts index 33071792138..04d31a75fdf 100644 --- a/src/__swaps__/utils/swaps.ts +++ b/src/__swaps__/utils/swaps.ts @@ -9,7 +9,7 @@ import { SLIDER_WIDTH, STABLECOIN_MINIMUM_SIGNIFICANT_DECIMALS, } from '@/__swaps__/screens/Swap/constants'; -import { ChainId, ChainName } from '@/__swaps__/types/chains'; +import { ChainId, ChainName } from '@/networks/types'; import { chainNameFromChainId, chainNameFromChainIdWorklet } from '@/__swaps__/utils/chains'; import { isLowerCaseMatchWorklet } from '@/__swaps__/utils/strings'; import { globalColors } from '@/design-system'; @@ -252,7 +252,7 @@ export function niceIncrementFormatter({ const niceIncrement = findNiceIncrement(inputAssetBalance); const incrementDecimalPlaces = countDecimalPlaces(niceIncrement); - if (percentageToSwap === 0 || equalWorklet(niceIncrement, 0)) return '0'; + if (percentageToSwap === 0 || equalWorklet(niceIncrement, 0)) return 0; if (percentageToSwap === 0.25) { const amount = mulWorklet(inputAssetBalance, 0.25); return valueBasedDecimalFormatter({ diff --git a/src/__swaps__/utils/userChains.ts b/src/__swaps__/utils/userChains.ts index 0a9a35369c1..cff0ccf7f6e 100644 --- a/src/__swaps__/utils/userChains.ts +++ b/src/__swaps__/utils/userChains.ts @@ -21,7 +21,7 @@ import { sepolia, } from 'viem/chains'; -import { ChainId, ChainNameDisplay } from '@/__swaps__/types/chains'; +import { ChainId, ChainNameDisplay } from '@/networks/types'; export const chainIdMap: Record< ChainId.mainnet | ChainId.optimism | ChainId.polygon | ChainId.base | ChainId.bsc | ChainId.zora | ChainId.avalanche, diff --git a/src/analytics/event.ts b/src/analytics/event.ts index ebb6849433e..22c3c8c5e93 100644 --- a/src/analytics/event.ts +++ b/src/analytics/event.ts @@ -1,13 +1,12 @@ import { GasSettings } from '@/__swaps__/screens/Swap/hooks/useCustomGas'; import { ExtendedAnimatedAssetWithColors, ParsedSearchAsset } from '@/__swaps__/types/assets'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId, Network } from '@/networks/types'; import { GasSpeed } from '@/__swaps__/types/gas'; import { SwapAssetType } from '@/__swaps__/types/swap'; import { UnlockableAppIconKey } from '@/appIcons/appIcons'; import { CardType } from '@/components/cards/GenericCard'; import { LearnCategory } from '@/components/cards/utils/types'; import { FiatProviderName } from '@/entities/f2c'; -import { Network } from '@/networks/types'; import { RapSwapActionParameters } from '@/raps/references'; import { RequestSource } from '@/utils/requestNavigationHandlers'; import { CrosschainQuote, Quote, QuoteError } from '@rainbow-me/swaps'; diff --git a/src/appIcons/appIcons.ts b/src/appIcons/appIcons.ts index 025ed695e05..407b688f21b 100644 --- a/src/appIcons/appIcons.ts +++ b/src/appIcons/appIcons.ts @@ -1,5 +1,4 @@ import { EthereumAddress } from '@/entities'; -import { Network } from '@/helpers'; import { ImageSourcePropType } from 'react-native'; import AppIconFiniliar from '@/assets/appIconFiniliar.png'; import AppIconGoldDoge from '@/assets/appIconGoldDoge.png'; @@ -15,6 +14,7 @@ import AppIconPoolboy from '@/assets/appIconPoolboy.png'; import AppIconAdworld from '@/assets/appIconAdworld.png'; import AppIconFarcaster from '@/assets/appIconFarcaster.png'; import { TokenGateCheckerNetwork } from '@/featuresToUnlock/tokenGatedUtils'; +import { Network } from '@/networks/types'; // optimism app icon unlocking NFTs const OPTIMISTIC_EXPLORER_NFT_ADDRESS: EthereumAddress = '0x81b30ff521D1fEB67EDE32db726D95714eb00637'; diff --git a/src/components/AddFundsInterstitial.js b/src/components/AddFundsInterstitial.js index 34312f9026a..23f430fd577 100644 --- a/src/components/AddFundsInterstitial.js +++ b/src/components/AddFundsInterstitial.js @@ -3,7 +3,6 @@ import lang from 'i18n-js'; import React, { Fragment, useCallback } from 'react'; import { Linking, View } from 'react-native'; import networkInfo from '../helpers/networkInfo'; -import networkTypes from '../helpers/networkTypes'; import showWalletErrorAlert from '../helpers/support'; import { useNavigation } from '../navigation/Navigation'; import { useTheme } from '../theme/ThemeContext'; @@ -20,6 +19,7 @@ import styled from '@/styled-thing'; import { padding, position } from '@/styles'; import ShadowStack from '@/react-native-shadow-stack'; import { useRoute } from '@react-navigation/native'; +import { Network } from '@/networks/types'; const ContainerWidth = 261; @@ -213,7 +213,7 @@ const AddFundsInterstitial = ({ network }) => { return ( - {network === networkTypes.mainnet ? ( + {network === Network.mainnet ? ( {ios ? lang.t('add_funds.to_get_started_ios') : lang.t('add_funds.to_get_started_android')} diff --git a/src/components/AppStateChangeHandler.tsx b/src/components/AppStateChangeHandler.tsx new file mode 100644 index 00000000000..ee8b29d2ebb --- /dev/null +++ b/src/components/AppStateChangeHandler.tsx @@ -0,0 +1,38 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; +import { AppState, AppStateStatus, Linking } from 'react-native'; +import { analyticsV2 } from '@/analytics'; +import store from '@/redux/store'; +import { walletConnectLoadState } from '@/redux/walletconnect'; + +type AppStateChangeHandlerProps = { + walletReady: boolean; +}; + +export function AppStateChangeHandler({ walletReady }: AppStateChangeHandlerProps) { + const [appState, setAppState] = useState(AppState.currentState); + const eventSubscription = useRef | null>(null); + + const handleAppStateChange = useCallback( + (nextAppState: AppStateStatus) => { + if (appState === 'background' && nextAppState === 'active') { + store.dispatch(walletConnectLoadState()); + } + setAppState(nextAppState); + analyticsV2.track(analyticsV2.event.appStateChange, { + category: 'app state', + label: nextAppState, + }); + }, + [appState] + ); + + useEffect(() => { + if (!walletReady) return; + + eventSubscription.current = AppState.addEventListener('change', handleAppStateChange); + + return () => eventSubscription.current?.remove(); + }, [handleAppStateChange]); + + return null; +} diff --git a/src/components/ChainLogo.js b/src/components/ChainLogo.js index ef03bb368c7..ae21dddd071 100644 --- a/src/components/ChainLogo.js +++ b/src/components/ChainLogo.js @@ -19,9 +19,9 @@ import BaseBadge from '../assets/badges/baseBadge.png'; import BaseBadgeDark from '../assets/badges/baseBadgeDark.png'; import BaseBadgeNoShadow from '../assets/badges/baseBadgeNoShadow.png'; import { Centered } from './layout'; -import networkTypes from '@/helpers/networkTypes'; import styled from '@/styled-thing'; import { position } from '@/styles'; +import { ChainId } from '@/networks/types'; const ChainIcon = styled(FastImage)({ height: ({ size }) => size, @@ -34,25 +34,25 @@ const Content = styled(Centered)(({ size }) => ({ overflow: 'visible', })); -export default function ChainLogo({ network, size = 44, withShadows = true, ...props }) { +export default function ChainLogo({ chainId, size = 44, withShadows = true, ...props }) { const { isDarkMode } = useTheme(); const source = useMemo(() => { let val = null; - if (network === networkTypes.arbitrum) { + if (chainId === ChainId.arbitrum) { val = withShadows ? (isDarkMode ? ArbitrumBadgeDark : ArbitrumBadge) : ArbitrumBadgeNoShadow; - } else if (network === networkTypes.optimism) { + } else if (chainId === ChainId.optimism) { val = withShadows ? (isDarkMode ? OptimismBadgeDark : OptimismBadge) : OptimismBadgeNoShadow; - } else if (network === networkTypes.polygon) { + } else if (chainId === ChainId.polygon) { val = withShadows ? (isDarkMode ? PolygonBadgeDark : PolygonBadge) : PolygonBadgeNoShadow; - } else if (network === networkTypes.bsc) { + } else if (chainId === ChainId.bsc) { val = withShadows ? (isDarkMode ? BscBadgeDark : BscBadge) : BscBadgeNoShadow; - } else if (network === networkTypes.zora) { + } else if (chainId === ChainId.zora) { val = withShadows ? (isDarkMode ? ZoraBadgeDark : ZoraBadge) : ZoraBadgeNoShadow; - } else if (network === networkTypes.base) { + } else if (chainId === ChainId.base) { val = withShadows ? (isDarkMode ? BaseBadgeDark : BaseBadge) : BaseBadgeNoShadow; } return val; - }, [isDarkMode, network, withShadows]); + }, [isDarkMode, chainId, withShadows]); if (!source) return null; diff --git a/src/components/ContactRowInfoButton.js b/src/components/ContactRowInfoButton.js index 60c58c64a07..28ce8a1f159 100644 --- a/src/components/ContactRowInfoButton.js +++ b/src/components/ContactRowInfoButton.js @@ -69,7 +69,7 @@ const ContactRowActions = { const buildBlockExplorerAction = chainId => { const blockExplorerText = lang.t('wallet.action.view_on', { - blockExplorerName: startCase(ethereumUtils.getBlockExplorer(chainId)), + blockExplorerName: startCase(ethereumUtils.getBlockExplorer({ chainId })), }); return { @@ -82,7 +82,7 @@ const buildBlockExplorerAction = chainId => { }; }; -const ContactRowInfoButton = ({ children, item, network, scaleTo }) => { +const ContactRowInfoButton = ({ children, item, chainId, scaleTo }) => { const { setClipboard } = useClipboard(); const handleCopyAddress = useCallback( address => { @@ -93,7 +93,7 @@ const ContactRowInfoButton = ({ children, item, network, scaleTo }) => { ); const onPressAndroid = useCallback(() => { - const blockExplorerText = `View on ${startCase(ethereumUtils.getBlockExplorer(ethereumUtils.getChainIdFromNetwork(item?.network)))}`; + const blockExplorerText = `View on ${startCase(ethereumUtils.getBlockExplorer({ chainId }))}`; const androidContractActions = [lang.t('wallet.action.copy_contract_address'), blockExplorerText, lang.t('button.cancel')]; showActionSheetWithOptions( { @@ -107,14 +107,14 @@ const ContactRowInfoButton = ({ children, item, network, scaleTo }) => { handleCopyAddress(item?.address); } if (idx === 1) { - ethereumUtils.openAddressInBlockExplorer(item?.address, network); + ethereumUtils.openAddressInBlockExplorer({ address: item?.address, chainId }); } } ); - }, [item?.network, item?.name, item?.address, handleCopyAddress, network]); + }, [item?.name, item?.address, handleCopyAddress, chainId]); const menuConfig = useMemo(() => { - const blockExplorerAction = buildBlockExplorerAction(ethereumUtils.getChainIdFromNetwork(item?.network)); + const blockExplorerAction = buildBlockExplorerAction(chainId); return { menuItems: [ blockExplorerAction, @@ -125,17 +125,17 @@ const ContactRowInfoButton = ({ children, item, network, scaleTo }) => { ], menuTitle: `${item?.name}`, }; - }, [item]); + }, [chainId, item?.address, item?.name]); const handlePressMenuItem = useCallback( ({ nativeEvent: { actionKey } }) => { if (actionKey === ContactRowActionsEnum.copyAddress) { handleCopyAddress(item?.address); } else if (actionKey === ContactRowActionsEnum.blockExplorer) { - ethereumUtils.openAddressInBlockExplorer(item?.address); + ethereumUtils.openAddressInBlockExplorer({ address: item?.address, chainId }); } }, - [item, handleCopyAddress] + [handleCopyAddress, item?.address, chainId] ); const Container = children ? Centered : InfoButton; diff --git a/src/components/DappBrowser/Dimensions.ts b/src/components/DappBrowser/Dimensions.ts index a367b0967aa..2cddb184f37 100644 --- a/src/components/DappBrowser/Dimensions.ts +++ b/src/components/DappBrowser/Dimensions.ts @@ -2,11 +2,11 @@ import { IS_ANDROID, IS_IOS } from '@/env'; import { TAB_BAR_HEIGHT } from '@/navigation/SwipeNavigator'; import { deviceUtils, safeAreaInsetValues } from '@/utils'; import { StatusBar } from 'react-native'; -import { isUsingButtonNavigation } from '@/helpers/statusBarHelper'; +import { NAVIGATION_BAR_HEIGHT } from '@/utils/deviceUtils'; export const BOTTOM_BAR_HEIGHT = 88; export const TOP_INSET = IS_IOS ? safeAreaInsetValues.top : StatusBar.currentHeight ?? 40; -export const BOTTOM_INSET = IS_ANDROID ? (isUsingButtonNavigation() ? 32 : 12) : BOTTOM_BAR_HEIGHT; +export const BOTTOM_INSET = IS_ANDROID ? NAVIGATION_BAR_HEIGHT - 8 : BOTTOM_BAR_HEIGHT; export const WEBVIEW_HEIGHT = deviceUtils.dimensions.height - TOP_INSET - TAB_BAR_HEIGHT - BOTTOM_INSET; export const COLLAPSED_WEBVIEW_ASPECT_RATIO = 4 / 3; export const COLLAPSED_WEBVIEW_HEIGHT_UNSCALED = Math.min(WEBVIEW_HEIGHT, deviceUtils.dimensions.width * COLLAPSED_WEBVIEW_ASPECT_RATIO); diff --git a/src/components/DappBrowser/constants.ts b/src/components/DappBrowser/constants.ts index 482bfd23962..e3bdae2b9bd 100644 --- a/src/components/DappBrowser/constants.ts +++ b/src/components/DappBrowser/constants.ts @@ -14,7 +14,7 @@ export const USER_AGENT = { }; export const USER_AGENT_APPLICATION_NAME = 'Rainbow'; -const BLANK_BASE64_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='; +export const BLANK_BASE64_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='; export const TAB_SCREENSHOT_FASTER_IMAGE_CONFIG: Partial = { // This placeholder avoids an occasional loading spinner flash diff --git a/src/components/DappBrowser/control-panel/ControlPanel.tsx b/src/components/DappBrowser/control-panel/ControlPanel.tsx index 34a8421d1e8..55de3b4bf14 100644 --- a/src/components/DappBrowser/control-panel/ControlPanel.tsx +++ b/src/components/DappBrowser/control-panel/ControlPanel.tsx @@ -30,7 +30,6 @@ import { IS_ANDROID, IS_IOS } from '@/env'; import { removeFirstEmojiFromString, returnStringFirstEmoji } from '@/helpers/emojiHandler'; import { useAccountSettings, useInitializeWallet, useWallets, useWalletsWithBalancesAndNames } from '@/hooks'; import { useSyncSharedValue } from '@/hooks/reanimated/useSyncSharedValue'; -import { Network } from '@/networks/types'; import { useBrowserStore } from '@/state/browser/browserStore'; import { colors } from '@/styles'; import { deviceUtils, watchingAlert } from '@/utils'; @@ -41,7 +40,7 @@ import { TOP_INSET } from '../Dimensions'; import { formatUrl } from '../utils'; import { RouteProp, useRoute } from '@react-navigation/native'; import { toHex } from 'viem'; -import { RainbowNetworks } from '@/networks'; +import { RainbowNetworkObjects } from '@/networks'; import * as i18n from '@/languages'; import { useDispatch } from 'react-redux'; import store from '@/redux/store'; @@ -63,7 +62,7 @@ import { SWAPS_V2, useExperimentalFlag } from '@/config'; import { swapsStore } from '@/state/swaps/swapsStore'; import { userAssetsStore } from '@/state/assets/userAssets'; import { greaterThan } from '@/helpers/utilities'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; const PAGES = { HOME: 'home', @@ -102,7 +101,7 @@ export const ControlPanel = () => { hostSessions && hostSessions.sessions?.[hostSessions.activeSessionAddress] ? { address: hostSessions.activeSessionAddress, - network: hostSessions.sessions[hostSessions.activeSessionAddress], + chainId: hostSessions.sessions[hostSessions.activeSessionAddress], } : null, [hostSessions] @@ -112,7 +111,7 @@ export const ControlPanel = () => { const [currentAddress, setCurrentAddress] = useState( currentSession?.address || hostSessions?.activeSessionAddress || accountAddress ); - const [currentNetwork, setCurrentNetwork] = useState(currentSession?.network || Network.mainnet); + const [currentChainId, setCurrentChainId] = useState(currentSession?.chainId || ChainId.mainnet); // listens to the current active tab and sets the account useEffect(() => { @@ -128,8 +127,8 @@ export const ControlPanel = () => { setCurrentAddress(accountAddress); } - if (currentSession?.network) { - setCurrentNetwork(currentSession?.network); + if (currentSession?.chainId) { + setCurrentChainId(currentSession?.chainId); } } }, [accountAddress, activeTabHost, currentSession]); @@ -184,28 +183,27 @@ export const ControlPanel = () => { const { testnetsEnabled } = store.getState().settings; const allNetworkItems = useMemo(() => { - return RainbowNetworks.filter( + return RainbowNetworkObjects.filter( ({ networkType, features: { walletconnect } }) => walletconnect && (testnetsEnabled || networkType !== 'testnet') ).map(network => { return { IconComponent: , label: network.name, secondaryLabel: i18n.t( - isConnected && network.value === currentNetwork + isConnected && network.id === currentChainId ? i18n.l.dapp_browser.control_panel.connected : i18n.l.dapp_browser.control_panel.not_connected ), - uniqueId: network.value, - selected: network.value === currentNetwork, - chainId: network.id, + uniqueId: String(network.id), + selected: network.id === currentChainId, }; }); - }, [currentNetwork, isConnected, testnetsEnabled]); + }, [currentChainId, isConnected, testnetsEnabled]); const selectedWallet = allWalletItems.find(item => item.selected); const animatedAccentColor = useSharedValue(selectedWallet?.color || globalColors.blue10); - const selectedNetworkId = useSharedValue(currentNetwork?.toString() || RainbowNetworks[0].value); + const selectedNetworkId = useSharedValue(currentChainId?.toString() || RainbowNetworkObjects[0].value); const selectedWalletId = useSharedValue(selectedWallet?.uniqueId || accountAddress); const handleSwitchWallet = useCallback( @@ -213,21 +211,21 @@ export const ControlPanel = () => { const address = selectedItemId; updateActiveSession({ host: activeTabHost, address: address as `0x${string}` }); if (isConnected) { - updateActiveSessionNetwork({ host: activeTabHost, network: currentNetwork }); + updateActiveSessionNetwork({ host: activeTabHost, chainId: currentChainId }); // need to emit these events to the dapp activeTabRef.current?.injectJavaScript(`window.ethereum.emit('accountsChanged', ['${address}']); true;`); } setCurrentAddress(address); }, - [activeTabHost, activeTabRef, currentNetwork, isConnected, updateActiveSession, updateActiveSessionNetwork] + [activeTabHost, activeTabRef, currentChainId, isConnected, updateActiveSession, updateActiveSessionNetwork] ); const handleNetworkSwitch = useCallback( (selectedItemId: string) => { - updateActiveSessionNetwork({ host: activeTabHost, network: selectedItemId as Network }); - const chainId = RainbowNetworks.find(({ value }) => value === (selectedItemId as Network))?.id as number; + updateActiveSessionNetwork({ host: activeTabHost, chainId: Number(selectedItemId) as ChainId }); + const chainId = RainbowNetworkObjects.find(({ id }) => id === (Number(selectedItemId) as ChainId))?.id as number; activeTabRef.current?.injectJavaScript(`window.ethereum.emit('chainChanged', ${toHex(chainId)}); true;`); - setCurrentNetwork(selectedItemId as Network); + setCurrentChainId(Number(selectedItemId) as ChainId); }, [activeTabHost, activeTabRef, updateActiveSessionNetwork] ); @@ -235,23 +233,21 @@ export const ControlPanel = () => { const handleConnect = useCallback(async () => { const activeTabHost = getDappHost(activeTabUrl || ''); const address = selectedWalletId.value; - const network = selectedNetworkId.value as Network; + const chainId = Number(selectedNetworkId.value); addSession({ host: activeTabHost || '', address: address as `0x${string}`, - network, + chainId, url: activeTabUrl || '', }); - const chainId = ethereumUtils.getChainIdFromNetwork(network); - activeTabRef.current?.injectJavaScript( `window.ethereum.emit('accountsChanged', ['${address}']); window.ethereum.emit('connect', { address: '${address}', chainId: '${toHex(chainId)}' }); true;` ); setIsConnected(true); setCurrentAddress(address); - setCurrentNetwork(network); + setCurrentChainId(chainId); }, [activeTabUrl, selectedWalletId.value, selectedNetworkId.value, addSession, activeTabRef]); const handleDisconnect = useCallback(() => { @@ -273,7 +269,7 @@ export const ControlPanel = () => { ; goToPage: (pageId: string) => void; - selectedNetwork: string; + selectedChainId: ChainId; selectedWallet: ControlPanelMenuItemProps | undefined; allNetworkItems: ControlPanelMenuItemProps[]; isConnected: boolean; @@ -393,8 +389,8 @@ const HomePanel = ({ const walletLabel = selectedWallet?.label || ''; const walletSecondaryLabel = selectedWallet?.secondaryLabel || ''; - const network = allNetworkItems.find(item => item.uniqueId === selectedNetwork); - const networkIcon = ; + const network = allNetworkItems.find(item => item.uniqueId === String(selectedChainId)); + const networkIcon = ; const networkLabel = network?.label || ''; const networkSecondaryLabel = network?.secondaryLabel || ''; @@ -420,7 +416,7 @@ const HomePanel = ({ /> ); - }, [allNetworkItems, animatedAccentColor, goToPage, selectedNetwork, selectedWallet]); + }, [allNetworkItems, animatedAccentColor, goToPage, selectedChainId, selectedWallet]); const runWalletChecksBeforeSwapOrBridge = useCallback(async () => { if (!selectedWallet || !wallets) return false; @@ -460,7 +456,7 @@ const HomePanel = ({ return; } - const mainnetEth = await ethereumUtils.getNativeAssetForNetwork(ChainId.mainnet, selectedWallet?.uniqueId); + const mainnetEth = await ethereumUtils.getNativeAssetForNetwork({ chainId: ChainId.mainnet, address: selectedWallet?.uniqueId }); Navigation.handleAction(Routes.EXCHANGE_MODAL, { fromDiscover: true, params: { @@ -488,7 +484,7 @@ const HomePanel = ({ return; } - const mainnetEth = await ethereumUtils.getNativeAssetForNetwork(ChainId.mainnet, selectedWallet?.uniqueId); + const mainnetEth = await ethereumUtils.getNativeAssetForNetwork({ chainId: ChainId.mainnet, address: selectedWallet?.uniqueId }); Navigation.handleAction(Routes.EXCHANGE_MODAL, { fromDiscover: true, params: { @@ -737,7 +733,6 @@ interface ControlPanelMenuItemProps { label: string; labelColor?: TextColor; imageUrl?: string; - chainId?: ChainId; color?: string; onPress?: () => void; secondaryLabel?: string; diff --git a/src/components/DappBrowser/handleProviderRequest.ts b/src/components/DappBrowser/handleProviderRequest.ts index 3554f3ce19f..d0ce9356952 100644 --- a/src/components/DappBrowser/handleProviderRequest.ts +++ b/src/components/DappBrowser/handleProviderRequest.ts @@ -4,20 +4,18 @@ import * as lang from '@/languages'; import { Provider } from '@ethersproject/providers'; -import { RainbowNetworks, getNetworkObj } from '@/networks'; -import { getProviderForNetwork } from '@/handlers/web3'; -import ethereumUtils, { getNetworkFromChainId } from '@/utils/ethereumUtils'; +import { RainbowNetworkObjects, RainbowSupportedChainIds } from '@/networks'; +import { getProvider } from '@/handlers/web3'; import { UserRejectedRequestError } from 'viem'; import { convertHexToString } from '@/helpers/utilities'; import { logger } from '@/logger'; import { ActiveSession } from '@rainbow-me/provider/dist/references/appSession'; -import { Network } from '@/helpers'; import { handleDappBrowserConnectionPrompt, handleDappBrowserRequest } from '@/utils/requestNavigationHandlers'; import { Tab } from '@rainbow-me/provider/dist/references/messengers'; import { getDappMetadata } from '@/resources/metadata/dapp'; import { useAppSessionsStore } from '@/state/appSessions'; import { BigNumber } from '@ethersproject/bignumber'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; export type ProviderRequestPayload = RequestArguments & { id: number; @@ -123,56 +121,51 @@ const messengerProviderRequestFn = async (messenger: Messenger, request: Provide hostSessions && hostSessions.sessions?.[hostSessions.activeSessionAddress] ? { address: hostSessions.activeSessionAddress, - network: hostSessions.sessions[hostSessions.activeSessionAddress], + chainId: hostSessions.sessions[hostSessions.activeSessionAddress], } : null; - // Wait for response from the popup. - let response: unknown | null; - if (request.method === 'eth_requestAccounts') { const dappData = await getDappMetadata({ url: getDappHost(request.meta?.sender.url) }); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - chainId is not defined in the type const chainId = request.params?.[0]?.chainId ? BigNumber.from(request.params?.[0]?.chainId).toNumber() : undefined; - response = await handleDappBrowserConnectionPrompt({ + const response = await handleDappBrowserConnectionPrompt({ dappName: dappData?.appName || request.meta?.sender.title || '', dappUrl: request.meta?.sender.url || '', chainId, address: hostSessions?.activeSessionAddress || undefined, }); + if (!response || response instanceof Error) { + throw new UserRejectedRequestError(Error('User rejected the request.')); + } + useAppSessionsStore.getState().addSession({ host: getDappHost(request.meta?.sender.url) || '', - // @ts-ignore - address: response.address, - // @ts-ignore - network: getNetworkFromChainId(response.chainId), - // @ts-ignore + address: response?.address, + chainId: response.chainId, url: request.meta?.sender.url || '', }); + return response; } else { const dappData = await getDappMetadata({ url: getDappHost(request.meta?.sender.url) }); - response = await handleDappBrowserRequest({ + const response = await handleDappBrowserRequest({ dappName: dappData?.appName || request.meta?.sender.title || request.meta?.sender.url || '', imageUrl: dappData?.appLogo || '', address: appSession?.address || '', dappUrl: request.meta?.sender.url || '', payload: request, - chainId: appSession?.network ? ethereumUtils.getChainIdFromNetwork(appSession.network) : ChainId.mainnet, + chainId: appSession?.chainId ? appSession?.chainId : ChainId.mainnet, }); + return response as object; } - - if (!response) { - throw new UserRejectedRequestError(Error('User rejected the request.')); - } - return response; }; const isSupportedChainId = (chainId: number | string) => { const numericChainId = BigNumber.from(chainId).toNumber(); - return !!RainbowNetworks.find(network => Number(network.id) === numericChainId); + return !!RainbowSupportedChainIds.find(chainId => chainId === numericChainId); }; const getActiveSession = ({ host }: { host: string }): ActiveSession => { const hostSessions = useAppSessionsStore.getState().getActiveSession({ host }); @@ -180,26 +173,19 @@ const getActiveSession = ({ host }: { host: string }): ActiveSession => { hostSessions && hostSessions.sessions?.[hostSessions.activeSessionAddress] ? { address: hostSessions.activeSessionAddress, - network: hostSessions.sessions[hostSessions.activeSessionAddress], + chainId: hostSessions.sessions[hostSessions.activeSessionAddress], } : null; if (!appSession) return null; return { address: appSession?.address || '', - chainId: getChainIdByNetwork(appSession.network), + chainId: appSession.chainId, }; // return null; }; -const getChainIdByNetwork = (network: Network) => getNetworkObj(network).id; - -const getChain = (chainId: number) => RainbowNetworks.find(network => Number(network.id) === chainId); - -const getProvider = ({ chainId }: { chainId?: number | undefined }) => { - const network = getNetworkFromChainId(chainId || 1); - return getProviderForNetwork(network) as unknown as Provider; -}; +const getChain = (chainId: number) => RainbowNetworkObjects.find(network => Number(network.id) === chainId); const checkRateLimitFn = async (host: string) => { // try { @@ -284,7 +270,7 @@ export const handleProviderRequestApp = ({ messenger, data, meta }: { messenger: callbackOptions?: CallbackOptions; }): { chainAlreadyAdded: boolean } => { const { chainId } = proposedChain; - const supportedChains = RainbowNetworks.filter(network => network.features.walletconnect).map(network => network.id.toString()); + const supportedChains = RainbowNetworkObjects.filter(network => network.features.walletconnect).map(network => network.id.toString()); const numericChainId = convertHexToString(chainId); if (supportedChains.includes(numericChainId)) { // TODO - Open add / switch ethereum chain @@ -341,14 +327,14 @@ export const handleProviderRequestApp = ({ messenger, data, meta }: { messenger: callbackOptions?: CallbackOptions; }) => { const { chainId } = proposedChain; - const supportedChains = RainbowNetworks.filter(network => network.features.walletconnect).map(network => network.id.toString()); + const supportedChains = RainbowNetworkObjects.filter(network => network.features.walletconnect).map(network => network.id.toString()); const numericChainId = convertHexToString(chainId); const supportedChainId = supportedChains.includes(numericChainId); if (supportedChainId) { const host = getDappHost(callbackOptions?.sender.url) || ''; const activeSession = getActiveSession({ host }); if (activeSession) { - useAppSessionsStore.getState().updateActiveSessionNetwork({ host: host, network: getNetworkFromChainId(Number(numericChainId)) }); + useAppSessionsStore.getState().updateActiveSessionNetwork({ host: host, chainId: Number(numericChainId) }); messenger.send(`chainChanged:${host}`, Number(numericChainId)); } console.warn('PROVIDER TODO: TODO SEND NOTIFICATION'); @@ -364,7 +350,7 @@ export const handleProviderRequestApp = ({ messenger, data, meta }: { messenger: checkRateLimit, onSwitchEthereumChainNotSupported, onSwitchEthereumChainSupported, - getProvider, + getProvider: chainId => getProvider({ chainId: chainId as number }) as unknown as Provider, getActiveSession, getChain, }); diff --git a/src/components/DeeplinkHandler.tsx b/src/components/DeeplinkHandler.tsx new file mode 100644 index 00000000000..e34f7ef4e8b --- /dev/null +++ b/src/components/DeeplinkHandler.tsx @@ -0,0 +1,65 @@ +import { useCallback, useEffect, useRef } from 'react'; +import { Linking } from 'react-native'; +import branch from 'react-native-branch'; +import { useMobileWalletProtocolHost } from '@coinbase/mobile-wallet-protocol-host'; +import handleDeeplink from '@/handlers/deeplinks'; +import { InitialRoute } from '@/navigation/initialRoute'; +import { logger, RainbowError } from '@/logger'; +import { branchListener } from '@/utils/branch'; + +type DeeplinkHandlerProps = { + initialRoute: InitialRoute; + walletReady: boolean; +}; + +export function DeeplinkHandler({ initialRoute, walletReady }: DeeplinkHandlerProps) { + const branchListenerRef = useRef | null>(null); + const { handleRequestUrl, sendFailureToClient } = useMobileWalletProtocolHost(); + + const setupDeeplinking = useCallback(async () => { + const initialUrl = await Linking.getInitialURL(); + + branchListenerRef.current = await branchListener(async url => { + logger.debug(`[App]: Branch listener called`, {}, logger.DebugContext.deeplinks); + + try { + handleDeeplink({ + url, + initialRoute, + handleRequestUrl, + sendFailureToClient, + }); + } catch (error) { + if (error instanceof Error) { + logger.error(new RainbowError('Error opening deeplink'), { + message: error.message, + url, + }); + } else { + logger.error(new RainbowError('Error opening deeplink'), { + message: 'Unknown error', + url, + }); + } + } + }); + + if (initialUrl) { + logger.debug(`[App]: has initial URL, opening with Branch`, { initialUrl }); + branch.openURL(initialUrl); + } + }, [handleRequestUrl, initialRoute, sendFailureToClient]); + + useEffect(() => { + if (!walletReady) return; + + setupDeeplinking(); + return () => { + if (branchListenerRef.current) { + branchListenerRef.current(); + } + }; + }, [setupDeeplinking, walletReady]); + + return null; +} diff --git a/src/components/FadeGradient.tsx b/src/components/FadeGradient.tsx new file mode 100644 index 00000000000..88fdade67a7 --- /dev/null +++ b/src/components/FadeGradient.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { StyleProp, ViewStyle } from 'react-native'; +import LinearGradient from 'react-native-linear-gradient'; +import Animated from 'react-native-reanimated'; + +import { Box, globalColors } from '@/design-system'; + +import { useTheme } from '@/theme'; + +type FadeGradientProps = { side: 'top' | 'bottom'; style?: StyleProp>> }; + +export const FadeGradient = ({ side, style }: FadeGradientProps) => { + const { colors, isDarkMode } = useTheme(); + + const isTop = side === 'top'; + const solidColor = isDarkMode ? globalColors.white10 : '#FBFCFD'; + const transparentColor = colors.alpha(solidColor, 0); + + return ( + + + + ); +}; diff --git a/src/components/FadedScrollCard.tsx b/src/components/FadedScrollCard.tsx new file mode 100644 index 00000000000..bf8a1ed9c39 --- /dev/null +++ b/src/components/FadedScrollCard.tsx @@ -0,0 +1,281 @@ +import React, { useCallback, useState } from 'react'; +import { TouchableWithoutFeedback } from 'react-native'; +import Animated, { + Easing, + SharedValue, + interpolate, + interpolateColor, + measure, + runOnJS, + runOnUI, + useAnimatedReaction, + useAnimatedRef, + useAnimatedStyle, + useSharedValue, + withTiming, +} from 'react-native-reanimated'; + +import { globalColors } from '@/design-system'; + +import { useTheme } from '@/theme'; + +import { useDimensions } from '@/hooks'; +import { FadeGradient } from '@/components/FadeGradient'; + +const COLLAPSED_CARD_HEIGHT = 56; +const MAX_CARD_HEIGHT = 176; + +const CARD_BORDER_WIDTH = 1.5; + +const timingConfig = { + duration: 300, + easing: Easing.bezier(0.2, 0, 0, 1), +}; + +type FadedScrollCardProps = { + cardHeight: SharedValue; + children: React.ReactNode; + contentHeight: SharedValue; + expandedCardBottomInset?: number; + expandedCardTopInset?: number; + initialScrollEnabled?: boolean; + isExpanded: boolean; + onPressCollapsedCard?: () => void; + skipCollapsedState?: boolean; +}; + +export const FadedScrollCard = ({ + cardHeight, + children, + contentHeight, + expandedCardBottomInset = 120, + expandedCardTopInset = 120, + initialScrollEnabled, + isExpanded, + onPressCollapsedCard, + skipCollapsedState, +}: FadedScrollCardProps) => { + const { height: deviceHeight, width: deviceWidth } = useDimensions(); + const { isDarkMode } = useTheme(); + + const cardRef = useAnimatedRef(); + + const [scrollEnabled, setScrollEnabled] = useState(initialScrollEnabled); + const [isFullyExpanded, setIsFullyExpanded] = useState(false); + + const yPosition = useSharedValue(0); + + const maxExpandedHeight = deviceHeight - (expandedCardBottomInset + expandedCardTopInset); + + const containerStyle = useAnimatedStyle(() => { + return { + height: + cardHeight.value > MAX_CARD_HEIGHT || !skipCollapsedState + ? interpolate( + cardHeight.value, + [MAX_CARD_HEIGHT, MAX_CARD_HEIGHT, maxExpandedHeight], + [cardHeight.value, MAX_CARD_HEIGHT, MAX_CARD_HEIGHT], + 'clamp' + ) + : undefined, + zIndex: interpolate(cardHeight.value, [0, MAX_CARD_HEIGHT, MAX_CARD_HEIGHT + 1], [1, 1, 2], 'clamp'), + }; + }); + + const backdropStyle = useAnimatedStyle(() => { + const canExpandFully = contentHeight.value + CARD_BORDER_WIDTH * 2 > MAX_CARD_HEIGHT; + return { + opacity: canExpandFully && isFullyExpanded ? withTiming(1, timingConfig) : withTiming(0, timingConfig), + }; + }); + + const cardStyle = useAnimatedStyle(() => { + const canExpandFully = contentHeight.value + CARD_BORDER_WIDTH * 2 > MAX_CARD_HEIGHT; + const expandedCardHeight = Math.min(contentHeight.value + CARD_BORDER_WIDTH * 2, maxExpandedHeight); + + const outputRange = [0, 0]; + + const yPos = -yPosition.value; + const offset = + deviceHeight - (expandedCardBottomInset + expandedCardTopInset) - expandedCardHeight - (yPosition.value + expandedCardHeight); + + if (yPos + expandedCardTopInset + offset >= deviceHeight - expandedCardBottomInset) { + outputRange.push(0); + } else { + outputRange.push(deviceHeight - expandedCardBottomInset - yPosition.value - expandedCardHeight); + } + + return { + borderColor: interpolateColor( + cardHeight.value, + [0, MAX_CARD_HEIGHT, expandedCardHeight], + isDarkMode ? ['#1F2023', '#1F2023', '#242527'] : ['#F5F7F8', '#F5F7F8', '#FBFCFD'] + ), + height: cardHeight.value > MAX_CARD_HEIGHT ? cardHeight.value : undefined, + position: canExpandFully && isFullyExpanded ? 'absolute' : 'relative', + transform: [ + { + translateY: interpolate(cardHeight.value, [0, MAX_CARD_HEIGHT, expandedCardHeight], outputRange), + }, + ], + }; + }); + + const centerVerticallyWhenCollapsedStyle = useAnimatedStyle(() => { + return { + transform: skipCollapsedState + ? undefined + : [ + { + translateY: interpolate( + cardHeight.value, + [ + 0, + COLLAPSED_CARD_HEIGHT, + contentHeight.value + CARD_BORDER_WIDTH * 2 > MAX_CARD_HEIGHT + ? MAX_CARD_HEIGHT + : contentHeight.value + CARD_BORDER_WIDTH * 2, + maxExpandedHeight, + ], + [-2, -2, 0, 0] + ), + }, + ], + }; + }); + + const shadowStyle = useAnimatedStyle(() => { + const canExpandFully = contentHeight.value + CARD_BORDER_WIDTH * 2 > MAX_CARD_HEIGHT; + return { + shadowOpacity: canExpandFully && isFullyExpanded ? withTiming(isDarkMode ? 0.9 : 0.16, timingConfig) : withTiming(0, timingConfig), + }; + }); + + const handleContentSizeChange = useCallback( + (width: number, height: number) => { + contentHeight.value = Math.round(height); + }, + [contentHeight] + ); + + const handleOnLayout = useCallback(() => { + runOnUI(() => { + if (cardHeight.value === MAX_CARD_HEIGHT) { + const measurement = measure(cardRef); + if (measurement === null) { + return; + } + if (yPosition.value !== measurement.pageY) { + yPosition.value = measurement.pageY; + } + } + })(); + }, [cardHeight, cardRef, yPosition]); + + useAnimatedReaction( + () => ({ contentHeight: contentHeight.value, isExpanded, isFullyExpanded }), + ({ contentHeight, isExpanded, isFullyExpanded }, previous) => { + if ( + isFullyExpanded !== previous?.isFullyExpanded || + isExpanded !== previous?.isExpanded || + contentHeight !== previous?.contentHeight + ) { + if (isFullyExpanded) { + const expandedCardHeight = + contentHeight + CARD_BORDER_WIDTH * 2 > maxExpandedHeight ? maxExpandedHeight : contentHeight + CARD_BORDER_WIDTH * 2; + if (contentHeight + CARD_BORDER_WIDTH * 2 > MAX_CARD_HEIGHT && cardHeight.value >= MAX_CARD_HEIGHT) { + cardHeight.value = withTiming(expandedCardHeight, timingConfig); + } else { + runOnJS(setIsFullyExpanded)(false); + } + } else if (isExpanded) { + cardHeight.value = withTiming( + contentHeight + CARD_BORDER_WIDTH * 2 > MAX_CARD_HEIGHT ? MAX_CARD_HEIGHT : contentHeight + CARD_BORDER_WIDTH * 2, + timingConfig + ); + } else { + cardHeight.value = withTiming(COLLAPSED_CARD_HEIGHT, timingConfig); + } + + const enableScroll = isExpanded && contentHeight + CARD_BORDER_WIDTH * 2 > (isFullyExpanded ? maxExpandedHeight : MAX_CARD_HEIGHT); + runOnJS(setScrollEnabled)(enableScroll); + } + } + ); + + return ( + + { + if (isFullyExpanded) { + setIsFullyExpanded(false); + } + }} + pointerEvents={isFullyExpanded ? 'auto' : 'none'} + style={[ + { + backgroundColor: 'rgba(0, 0, 0, 0.6)', + height: deviceHeight * 3, + left: -deviceWidth * 0.5, + position: 'absolute', + top: -deviceHeight, + width: deviceWidth * 2, + zIndex: -1, + }, + backdropStyle, + ]} + /> + + + + { + if (!isFullyExpanded) { + setIsFullyExpanded(true); + } else setIsFullyExpanded(false); + } + } + > + {children} + + + + + + + + ); +}; diff --git a/src/components/L2Disclaimer.js b/src/components/L2Disclaimer.js index 162084f1dec..aa5361e6970 100644 --- a/src/components/L2Disclaimer.js +++ b/src/components/L2Disclaimer.js @@ -7,14 +7,13 @@ import { Column, Row } from './layout'; import { Text } from './text'; import { padding, position } from '@/styles'; import { darkModeThemeColors } from '@/styles/colors'; -import { getNetworkObj } from '@/networks'; import * as lang from '@/languages'; import { isL2Chain } from '@/handlers/web3'; import { EthCoinIcon } from './coin-icon/EthCoinIcon'; -import { ethereumUtils } from '@/utils'; +import { chainIdToNameMapping } from '@/networks/types'; const L2Disclaimer = ({ - network, + chainId, colors, hideDivider, isNft = false, @@ -37,7 +36,6 @@ const L2Disclaimer = ({ }, }; - const chainId = ethereumUtils.getChainIdFromNetwork(network); const isL2 = isL2Chain({ chainId }); return ( @@ -59,7 +57,7 @@ const L2Disclaimer = ({ ? customText : lang.t(lang.l.expanded_state.asset.l2_disclaimer, { symbol, - network: getNetworkObj(network).name, + network: chainIdToNameMapping[chainId], })} diff --git a/src/components/MobileWalletProtocolListener.tsx b/src/components/MobileWalletProtocolListener.tsx new file mode 100644 index 00000000000..1c5dd960d4e --- /dev/null +++ b/src/components/MobileWalletProtocolListener.tsx @@ -0,0 +1,51 @@ +import { useEffect, useRef } from 'react'; +import { addDiagnosticLogListener, getAndroidIntentUrl, useMobileWalletProtocolHost } from '@coinbase/mobile-wallet-protocol-host'; +import { handleMobileWalletProtocolRequest } from '@/utils/requestNavigationHandlers'; +import { logger, RainbowError } from '@/logger'; +import { IS_ANDROID, IS_DEV } from '@/env'; + +export const MobileWalletProtocolListener = () => { + const { message, handleRequestUrl, sendFailureToClient, ...mwpProps } = useMobileWalletProtocolHost(); + const lastMessageUuidRef = useRef(null); + + useEffect(() => { + if (message && lastMessageUuidRef.current !== message.uuid) { + lastMessageUuidRef.current = message.uuid; + try { + handleMobileWalletProtocolRequest({ request: message, ...mwpProps }); + } catch (error) { + logger.error(new RainbowError('Error handling Mobile Wallet Protocol request'), { + error, + }); + } + } + }, [message, mwpProps]); + + useEffect(() => { + if (IS_DEV) { + const removeListener = addDiagnosticLogListener(event => { + logger.debug(`[MobileWalletProtocolListener]: Diagnostic log event: ${JSON.stringify(event, null, 2)}`); + }); + + return () => removeListener(); + } + }, []); + + useEffect(() => { + if (IS_ANDROID) { + (async function handleAndroidIntent() { + const intentUrl = await getAndroidIntentUrl(); + if (intentUrl) { + const response = await handleRequestUrl(intentUrl); + if (response.error) { + // Return error to client app if session is expired or invalid + const { errorMessage, decodedRequest } = response.error; + await sendFailureToClient(errorMessage, decodedRequest); + } + } + })(); + } + }, [handleRequestUrl, sendFailureToClient]); + + return null; +}; diff --git a/src/components/Transactions/TransactionDetailsCard.tsx b/src/components/Transactions/TransactionDetailsCard.tsx new file mode 100644 index 00000000000..307af8def77 --- /dev/null +++ b/src/components/Transactions/TransactionDetailsCard.tsx @@ -0,0 +1,127 @@ +import React, { useState } from 'react'; +import * as i18n from '@/languages'; +import Animated, { interpolate, useAnimatedStyle, useSharedValue } from 'react-native-reanimated'; + +import { Box, Inline, Stack, Text } from '@/design-system'; +import { TextColor } from '@/design-system/color/palettes'; + +import { abbreviations, ethereumUtils } from '@/utils'; +import { TransactionSimulationMeta } from '@/graphql/__generated__/metadataPOST'; +import { ChainId } from '@/networks/types'; + +import { getNetworkObject } from '@/networks'; +import { TransactionDetailsRow } from '@/components/Transactions/TransactionDetailsRow'; +import { FadedScrollCard } from '@/components/FadedScrollCard'; +import { IconContainer } from '@/components/Transactions/TransactionIcons'; +import { formatDate } from '@/utils/formatDate'; +import { + COLLAPSED_CARD_HEIGHT, + MAX_CARD_HEIGHT, + CARD_ROW_HEIGHT, + CARD_BORDER_WIDTH, + EXPANDED_CARD_TOP_INSET, +} from '@/components/Transactions/constants'; + +interface TransactionDetailsCardProps { + chainId: ChainId; + expandedCardBottomInset: number; + isBalanceEnough: boolean | undefined; + isLoading: boolean; + meta: TransactionSimulationMeta | undefined; + methodName: string; + noChanges: boolean; + nonce: string | undefined; + toAddress: string; +} + +export const TransactionDetailsCard = ({ + chainId, + expandedCardBottomInset, + isBalanceEnough, + isLoading, + meta, + methodName, + noChanges, + nonce, + toAddress, +}: TransactionDetailsCardProps) => { + const cardHeight = useSharedValue(COLLAPSED_CARD_HEIGHT); + const contentHeight = useSharedValue(COLLAPSED_CARD_HEIGHT - CARD_BORDER_WIDTH * 2); + const [isExpanded, setIsExpanded] = useState(false); + + const currentNetwork = getNetworkObject({ chainId }); + + const listStyle = useAnimatedStyle(() => ({ + opacity: interpolate( + cardHeight.value, + [ + COLLAPSED_CARD_HEIGHT, + contentHeight.value + CARD_BORDER_WIDTH * 2 > MAX_CARD_HEIGHT ? MAX_CARD_HEIGHT : contentHeight.value + CARD_BORDER_WIDTH * 2, + ], + [0, 1] + ), + })); + + const collapsedTextColor: TextColor = isLoading ? 'labelQuaternary' : 'blue'; + + const showFunctionRow = meta?.to?.function || (methodName && methodName.substring(0, 2) !== '0x'); + const isContract = showFunctionRow || meta?.to?.created || meta?.to?.sourceCodeStatus; + const showTransferToRow = !!meta?.transferTo?.address; + // Hide DetailsCard if balance is insufficient once loaded + if (!isLoading && isBalanceEnough === false) { + return <>; + } + return ( + setIsExpanded(true)} + > + + + + + + 􁙠 + + + + {i18n.t(i18n.l.walletconnect.simulation.details_card.title)} + + + + + + {} + {!!(meta?.to?.address || toAddress || showTransferToRow) && ( + + ethereumUtils.openAddressInBlockExplorer({ + address: meta?.to?.address || toAddress || meta?.transferTo?.address || '', + chainId, + }) + } + value={ + meta?.to?.name || + abbreviations.address(meta?.to?.address || toAddress, 4, 6) || + meta?.to?.address || + toAddress || + meta?.transferTo?.address || + '' + } + /> + )} + {showFunctionRow && } + {!!meta?.to?.sourceCodeStatus && } + {!!meta?.to?.created && } + {nonce && } + + + + + ); +}; diff --git a/src/components/Transactions/TransactionDetailsRow.tsx b/src/components/Transactions/TransactionDetailsRow.tsx new file mode 100644 index 00000000000..e32b6cdcd7a --- /dev/null +++ b/src/components/Transactions/TransactionDetailsRow.tsx @@ -0,0 +1,90 @@ +import React from 'react'; +import * as i18n from '@/languages'; +import { TouchableWithoutFeedback } from 'react-native'; + +import { ButtonPressAnimation } from '@/components/animations'; +import { ChainImage } from '@/components/coin-icon/ChainImage'; +import { Box, Inline, Text } from '@/design-system'; + +import { DetailIcon, DetailBadge, IconContainer } from '@/components/Transactions/TransactionIcons'; +import { SMALL_CARD_ROW_HEIGHT } from '@/components/Transactions/constants'; +import { DetailType, DetailInfo } from '@/components/Transactions/types'; +import { ChainId } from '@/networks/types'; + +interface TransactionDetailsRowProps { + chainId?: ChainId; + detailType: DetailType; + onPress?: () => void; + value: string; +} + +export const TransactionDetailsRow = ({ chainId, detailType, onPress, value }: TransactionDetailsRowProps) => { + const detailInfo: DetailInfo = infoForDetailType[detailType]; + + return ( + + + + + + {detailInfo.label} + + + + {detailType === 'function' && } + {detailType === 'sourceCodeVerification' && ( + + )} + {detailType === 'chain' && chainId && } + {detailType !== 'function' && detailType !== 'sourceCodeVerification' && ( + + {value} + + )} + {(detailType === 'contract' || detailType === 'to') && ( + + + + + 􀂄 + + + + + )} + + + + ); +}; + +const infoForDetailType: { [key: string]: DetailInfo } = { + chain: { + icon: '􀤆', + label: i18n.t(i18n.l.walletconnect.simulation.details_card.types.chain), + }, + contract: { + icon: '􀉆', + label: i18n.t(i18n.l.walletconnect.simulation.details_card.types.contract), + }, + to: { + icon: '􀉩', + label: i18n.t(i18n.l.walletconnect.simulation.details_card.types.to), + }, + function: { + icon: '􀡅', + label: i18n.t(i18n.l.walletconnect.simulation.details_card.types.function), + }, + sourceCodeVerification: { + icon: '􀕹', + label: i18n.t(i18n.l.walletconnect.simulation.details_card.types.source_code), + }, + dateCreated: { + icon: '􀉉', + label: i18n.t(i18n.l.walletconnect.simulation.details_card.types.contract_created), + }, + nonce: { + icon: '􀆃', + label: i18n.t(i18n.l.walletconnect.simulation.details_card.types.nonce), + }, +}; diff --git a/src/components/Transactions/TransactionIcons.tsx b/src/components/Transactions/TransactionIcons.tsx new file mode 100644 index 00000000000..53ef21fcd6e --- /dev/null +++ b/src/components/Transactions/TransactionIcons.tsx @@ -0,0 +1,196 @@ +import React from 'react'; +import { AnimatePresence, MotiView } from 'moti'; + +import { Bleed, Box, Text, globalColors, useForegroundColor } from '@/design-system'; +import { TextColor } from '@/design-system/color/palettes'; +import { infoForEventType, motiTimingConfig } from '@/components/Transactions/constants'; + +import { useTheme } from '@/theme'; +import { DetailInfo, EventInfo, EventType } from '@/components/Transactions/types'; + +export const EventIcon = ({ eventType }: { eventType: EventType }) => { + const eventInfo: EventInfo = infoForEventType[eventType]; + + const hideInnerFill = eventType === 'approve' || eventType === 'revoke'; + const isWarningIcon = + eventType === 'failed' || eventType === 'insufficientBalance' || eventType === 'MALICIOUS' || eventType === 'WARNING'; + + return ( + + {!hideInnerFill && ( + + )} + + {eventInfo.icon} + + + ); +}; + +export const DetailIcon = ({ detailInfo }: { detailInfo: DetailInfo }) => { + return ( + + + {detailInfo.icon} + + + ); +}; + +export const DetailBadge = ({ type, value }: { type: 'function' | 'unknown' | 'unverified' | 'verified'; value: string }) => { + const { colors, isDarkMode } = useTheme(); + const separatorTertiary = useForegroundColor('separatorTertiary'); + + const infoForBadgeType: { + [key: string]: { + backgroundColor: string; + borderColor: string; + label?: string; + text: TextColor; + textOpacity?: number; + }; + } = { + function: { + backgroundColor: 'transparent', + borderColor: isDarkMode ? separatorTertiary : colors.alpha(separatorTertiary, 0.025), + text: 'labelQuaternary', + }, + unknown: { + backgroundColor: 'transparent', + borderColor: isDarkMode ? separatorTertiary : colors.alpha(separatorTertiary, 0.025), + label: 'Unknown', + text: 'labelQuaternary', + }, + unverified: { + backgroundColor: isDarkMode ? colors.alpha(colors.red, 0.05) : globalColors.red10, + borderColor: colors.alpha(colors.red, 0.02), + label: 'Unverified', + text: 'red', + textOpacity: 0.76, + }, + verified: { + backgroundColor: isDarkMode ? colors.alpha(colors.green, 0.05) : globalColors.green10, + borderColor: colors.alpha(colors.green, 0.02), + label: 'Verified', + text: 'green', + textOpacity: 0.76, + }, + }; + + return ( + + + + {infoForBadgeType[type].label || value} + + + + ); +}; + +export const VerifiedBadge = () => { + return ( + + + + + 􀇻 + + + + ); +}; + +export const AnimatedCheckmark = ({ visible }: { visible: boolean }) => { + return ( + + {visible && ( + + + + + + 􀁣 + + + + + )} + + ); +}; + +export const IconContainer = ({ + children, + hitSlop, + opacity, + size = 20, +}: { + children: React.ReactNode; + hitSlop?: number; + opacity?: number; + size?: number; +}) => { + // Prevent wide icons from being clipped + const extraHorizontalSpace = 4; + + return ( + + + {children} + + + ); +}; diff --git a/src/components/Transactions/TransactionMessageCard.tsx b/src/components/Transactions/TransactionMessageCard.tsx new file mode 100644 index 00000000000..3e946bf6d6d --- /dev/null +++ b/src/components/Transactions/TransactionMessageCard.tsx @@ -0,0 +1,113 @@ +import React, { useCallback, useMemo, useState } from 'react'; +import * as i18n from '@/languages'; +import { TouchableWithoutFeedback } from 'react-native'; +import { useSharedValue } from 'react-native-reanimated'; + +import { ButtonPressAnimation } from '@/components/animations'; +import { Bleed, Box, Inline, Stack, Text } from '@/design-system'; + +import { useClipboard } from '@/hooks'; +import { logger } from '@/logger'; +import { isSignTypedData } from '@/utils/signingMethods'; + +import { RPCMethod } from '@/walletConnect/types'; +import { sanitizeTypedData } from '@/utils/signingUtils'; +import { + estimateMessageHeight, + MAX_CARD_HEIGHT, + CARD_ROW_HEIGHT, + CARD_BORDER_WIDTH, + EXPANDED_CARD_TOP_INSET, +} from '@/components/Transactions/constants'; +import { FadedScrollCard } from '@/components/FadedScrollCard'; +import { AnimatedCheckmark, IconContainer } from '@/components/Transactions/TransactionIcons'; + +type TransactionMessageCardProps = { + expandedCardBottomInset: number; + message: string; + method: RPCMethod; +}; + +export const TransactionMessageCard = ({ expandedCardBottomInset, message, method }: TransactionMessageCardProps) => { + const { setClipboard } = useClipboard(); + const [didCopy, setDidCopy] = useState(false); + + let displayMessage = message; + if (isSignTypedData(method)) { + try { + const parsedMessage = JSON.parse(message); + const sanitizedMessage = sanitizeTypedData(parsedMessage); + displayMessage = sanitizedMessage; + } catch (error) { + logger.warn(`[TransactionMessageCard]: Error parsing signed typed data for ${method}`, { + error, + }); + } + + displayMessage = JSON.stringify(displayMessage, null, 4); + } + + const estimatedMessageHeight = useMemo(() => estimateMessageHeight(displayMessage), [displayMessage]); + + const cardHeight = useSharedValue( + estimatedMessageHeight > MAX_CARD_HEIGHT ? MAX_CARD_HEIGHT : estimatedMessageHeight + CARD_BORDER_WIDTH * 2 + ); + const contentHeight = useSharedValue(estimatedMessageHeight); + + const handleCopyPress = useCallback( + (message: string) => { + if (didCopy) return; + setClipboard(message); + setDidCopy(true); + const copyTimer = setTimeout(() => { + setDidCopy(false); + }, 2000); + return () => clearTimeout(copyTimer); + }, + [didCopy, setClipboard] + ); + + return ( + MAX_CARD_HEIGHT} + isExpanded + skipCollapsedState + > + + + + + + 􀙤 + + + + {i18n.t(i18n.l.walletconnect.simulation.message_card.title)} + + + + handleCopyPress(message)}> + + + + + + {i18n.t(i18n.l.walletconnect.simulation.message_card.copy)} + + + + + + + + + {displayMessage} + + + + ); +}; diff --git a/src/components/Transactions/TransactionSimulatedEventRow.tsx b/src/components/Transactions/TransactionSimulatedEventRow.tsx new file mode 100644 index 00000000000..ce4d26052eb --- /dev/null +++ b/src/components/Transactions/TransactionSimulatedEventRow.tsx @@ -0,0 +1,109 @@ +import React, { useMemo } from 'react'; +import * as i18n from '@/languages'; +import { Image, PixelRatio } from 'react-native'; + +import { Bleed, Box, Inline, Text } from '@/design-system'; + +import { useTheme } from '@/theme'; +import { TransactionAssetType, TransactionSimulationAsset } from '@/graphql/__generated__/metadataPOST'; +import { Network } from '@/networks/types'; +import { convertAmountToNativeDisplay, convertRawAmountToBalance } from '@/helpers/utilities'; + +import { useAccountSettings } from '@/hooks'; + +import { maybeSignUri } from '@/handlers/imgix'; +import RainbowCoinIcon from '@/components/coin-icon/RainbowCoinIcon'; +import { useExternalToken } from '@/resources/assets/externalAssetsQuery'; +import { EventInfo, EventType } from '@/components/Transactions/types'; +import { infoForEventType, CARD_ROW_HEIGHT } from '@/components/Transactions/constants'; +import { EventIcon } from '@/components/Transactions/TransactionIcons'; +import { ethereumUtils } from '@/utils'; + +type TransactionSimulatedEventRowProps = { + amount: string | 'unlimited'; + asset: TransactionSimulationAsset | undefined; + eventType: EventType; + price?: number | undefined; +}; + +export const TransactionSimulatedEventRow = ({ amount, asset, eventType, price }: TransactionSimulatedEventRowProps) => { + const theme = useTheme(); + const { nativeCurrency } = useAccountSettings(); + + const chainId = ethereumUtils.getChainIdFromNetwork((asset?.network as Network) || Network.mainnet); + + const { data: externalAsset } = useExternalToken({ + address: asset?.assetCode || '', + chainId, + currency: nativeCurrency, + }); + + const eventInfo: EventInfo = infoForEventType[eventType]; + + const formattedAmount = useMemo(() => { + if (!asset) return; + + const nftFallbackSymbol = parseFloat(amount) > 1 ? 'NFTs' : 'NFT'; + const assetDisplayName = + asset?.type === TransactionAssetType.Nft ? asset?.name || asset?.symbol || nftFallbackSymbol : asset?.symbol || asset?.name; + const shortenedDisplayName = assetDisplayName.length > 12 ? `${assetDisplayName.slice(0, 12).trim()}…` : assetDisplayName; + + const displayAmount = + asset?.decimals === 0 + ? `${amount}${shortenedDisplayName ? ' ' + shortenedDisplayName : ''}` + : convertRawAmountToBalance(amount, { decimals: asset?.decimals || 18, symbol: shortenedDisplayName }, 3, true).display; + + const unlimitedApproval = `${i18n.t(i18n.l.walletconnect.simulation.simulation_card.event_row.unlimited)} ${asset?.symbol}`; + + return `${eventInfo.amountPrefix}${amount === 'UNLIMITED' ? unlimitedApproval : displayAmount}`; + }, [amount, asset, eventInfo?.amountPrefix]); + + const url = maybeSignUri(asset?.iconURL, { + fm: 'png', + w: 16 * PixelRatio.get(), + }); + + const showUSD = (eventType === 'send' || eventType === 'receive') && !!price; + + const formattedPrice = price && convertAmountToNativeDisplay(price, nativeCurrency); + + return ( + + + + + + + {eventInfo.label} + + {showUSD && ( + + {formattedPrice} + + )} + + + + + {asset?.type !== TransactionAssetType.Nft ? ( + + ) : ( + + )} + + + {formattedAmount} + + + + + ); +}; diff --git a/src/components/Transactions/TransactionSimulationCard.tsx b/src/components/Transactions/TransactionSimulationCard.tsx new file mode 100644 index 00000000000..e491aeaf6a7 --- /dev/null +++ b/src/components/Transactions/TransactionSimulationCard.tsx @@ -0,0 +1,314 @@ +import React, { useMemo } from 'react'; +import * as i18n from '@/languages'; +import Animated, { + interpolate, + useAnimatedReaction, + useAnimatedStyle, + useSharedValue, + withRepeat, + withTiming, +} from 'react-native-reanimated'; + +import { Box, Inline, Stack, Text } from '@/design-system'; +import { TextColor } from '@/design-system/color/palettes'; + +import { TransactionErrorType, TransactionSimulationResult, TransactionScanResultType } from '@/graphql/__generated__/metadataPOST'; + +import { getNetworkObject } from '@/networks'; +import { isEmpty } from 'lodash'; +import { TransactionSimulatedEventRow } from '@/components/Transactions/TransactionSimulatedEventRow'; +import { FadedScrollCard } from '@/components/FadedScrollCard'; +import { EventIcon, IconContainer } from '@/components/Transactions/TransactionIcons'; +import { + COLLAPSED_CARD_HEIGHT, + MAX_CARD_HEIGHT, + CARD_ROW_HEIGHT, + CARD_BORDER_WIDTH, + EXPANDED_CARD_TOP_INSET, + rotationConfig, + timingConfig, +} from '@/components/Transactions/constants'; +import { ChainId } from '@/networks/types'; + +interface TransactionSimulationCardProps { + chainId: ChainId; + expandedCardBottomInset: number; + isBalanceEnough: boolean | undefined; + isLoading: boolean; + txSimulationApiError: unknown; + isPersonalSignRequest: boolean; + noChanges: boolean; + simulation: TransactionSimulationResult | undefined; + simulationError: TransactionErrorType | undefined; + simulationScanResult: TransactionScanResultType | undefined; + walletBalance: { + amount: string | number; + display: string; + isLoaded: boolean; + symbol: string; + }; +} + +export const TransactionSimulationCard = ({ + chainId, + expandedCardBottomInset, + isBalanceEnough, + isLoading, + txSimulationApiError, + isPersonalSignRequest, + noChanges, + simulation, + simulationError, + simulationScanResult, + walletBalance, +}: TransactionSimulationCardProps) => { + const cardHeight = useSharedValue(COLLAPSED_CARD_HEIGHT); + const contentHeight = useSharedValue(COLLAPSED_CARD_HEIGHT - CARD_BORDER_WIDTH * 2); + const spinnerRotation = useSharedValue(0); + + const listStyle = useAnimatedStyle(() => ({ + opacity: noChanges + ? withTiming(1, timingConfig) + : interpolate( + cardHeight.value, + [ + COLLAPSED_CARD_HEIGHT, + contentHeight.value + CARD_BORDER_WIDTH * 2 > MAX_CARD_HEIGHT ? MAX_CARD_HEIGHT : contentHeight.value + CARD_BORDER_WIDTH * 2, + ], + [0, 1] + ), + })); + + const spinnerStyle = useAnimatedStyle(() => { + return { + transform: [{ rotate: `${spinnerRotation.value}deg` }], + }; + }); + + useAnimatedReaction( + () => ({ isLoading, isPersonalSignRequest }), + ({ isLoading, isPersonalSignRequest }, previous = { isLoading: false, isPersonalSignRequest: false }) => { + if (isLoading && !previous?.isLoading) { + spinnerRotation.value = withRepeat(withTiming(360, rotationConfig), -1, false); + } else if ( + (!isLoading && previous?.isLoading) || + (isPersonalSignRequest && !previous?.isPersonalSignRequest && previous?.isLoading) + ) { + spinnerRotation.value = withTiming(360, timingConfig); + } + }, + [isLoading, isPersonalSignRequest] + ); + const renderSimulationEventRows = useMemo(() => { + if (isBalanceEnough === false) return null; + + return ( + <> + {simulation?.approvals?.map(change => { + return ( + + ); + })} + {simulation?.out?.map(change => { + return ( + + ); + })} + {simulation?.in?.map(change => { + return ( + + ); + })} + + ); + }, [isBalanceEnough, simulation]); + + const titleColor: TextColor = useMemo(() => { + if (isLoading) { + return 'label'; + } + if (isBalanceEnough === false) { + return 'blue'; + } + if (noChanges || isPersonalSignRequest || txSimulationApiError) { + return 'labelQuaternary'; + } + if (simulationScanResult === TransactionScanResultType.Warning) { + return 'orange'; + } + if (simulationError || simulationScanResult === TransactionScanResultType.Malicious) { + return 'red'; + } + return 'label'; + }, [isBalanceEnough, isLoading, noChanges, simulationError, simulationScanResult, isPersonalSignRequest, txSimulationApiError]); + + const titleText = useMemo(() => { + if (isLoading) { + return i18n.t(i18n.l.walletconnect.simulation.simulation_card.titles.simulating); + } + if (isBalanceEnough === false) { + return i18n.t(i18n.l.walletconnect.simulation.simulation_card.titles.not_enough_native_balance, { symbol: walletBalance?.symbol }); + } + if (txSimulationApiError || isPersonalSignRequest) { + return i18n.t(i18n.l.walletconnect.simulation.simulation_card.titles.simulation_unavailable); + } + if (simulationScanResult === TransactionScanResultType.Warning) { + return i18n.t(i18n.l.walletconnect.simulation.simulation_card.titles.proceed_carefully); + } + if (simulationScanResult === TransactionScanResultType.Malicious) { + return i18n.t(i18n.l.walletconnect.simulation.simulation_card.titles.suspicious_transaction); + } + if (noChanges) { + return i18n.t(i18n.l.walletconnect.simulation.simulation_card.messages.no_changes); + } + if (simulationError) { + return i18n.t(i18n.l.walletconnect.simulation.simulation_card.titles.likely_to_fail); + } + return i18n.t(i18n.l.walletconnect.simulation.simulation_card.titles.simulation_result); + }, [ + isBalanceEnough, + isLoading, + noChanges, + simulationError, + simulationScanResult, + isPersonalSignRequest, + txSimulationApiError, + walletBalance?.symbol, + ]); + + const isExpanded = useMemo(() => { + if (isLoading || isPersonalSignRequest) { + return false; + } + const shouldExpandOnLoad = isBalanceEnough === false || (!isEmpty(simulation) && !noChanges) || !!simulationError; + return shouldExpandOnLoad; + }, [isBalanceEnough, isLoading, isPersonalSignRequest, noChanges, simulation, simulationError]); + + return ( + + + + + {!isLoading && (simulationError || isBalanceEnough === false || simulationScanResult !== TransactionScanResultType.Ok) ? ( + + ) : ( + + {!isLoading && noChanges && !isPersonalSignRequest ? ( + + {/* The extra space avoids icon clipping */} + {'􀻾 '} + + ) : ( + + + 􀬨 + + + )} + + )} + + {titleText} + + + {/* TODO: Unhide once we add explainer sheets */} + {/* + + + + + 􀁜 + + + + + */} + + + + {isBalanceEnough === false ? ( + + {i18n.t(i18n.l.walletconnect.simulation.simulation_card.messages.need_more_native, { + symbol: walletBalance?.symbol, + network: getNetworkObject({ chainId }).name, + })} + + ) : ( + <> + {isPersonalSignRequest && ( + + + {i18n.t(i18n.l.walletconnect.simulation.simulation_card.messages.unavailable_personal_sign)} + + + )} + {txSimulationApiError && ( + + {i18n.t(i18n.l.walletconnect.simulation.simulation_card.messages.tx_api_error)} + + )} + {simulationError && ( + + {i18n.t(i18n.l.walletconnect.simulation.simulation_card.messages.failed_to_simulate)} + + )} + {simulationScanResult === TransactionScanResultType.Warning && ( + + {i18n.t(i18n.l.walletconnect.simulation.simulation_card.messages.warning)}{' '} + + )} + {simulationScanResult === TransactionScanResultType.Malicious && ( + + {i18n.t(i18n.l.walletconnect.simulation.simulation_card.messages.malicious)} + + )} + + )} + {renderSimulationEventRows} + + + + + ); +}; diff --git a/src/components/Transactions/constants.ts b/src/components/Transactions/constants.ts new file mode 100644 index 00000000000..79e6e0d8df5 --- /dev/null +++ b/src/components/Transactions/constants.ts @@ -0,0 +1,121 @@ +import * as i18n from '@/languages'; +import { Screens } from '@/state/performance/operations'; +import { safeAreaInsetValues } from '@/utils'; +import { TransitionConfig } from 'moti'; +import { Easing } from 'react-native-reanimated'; +import { EventInfo } from '@/components/Transactions/types'; +import { RequestSource } from '@/utils/requestNavigationHandlers'; + +export const SCREEN_BOTTOM_INSET = safeAreaInsetValues.bottom + 20; +export const GAS_BUTTON_SPACE = + 30 + // GasSpeedButton height + 24; // Between GasSpeedButton and bottom of sheet + +export const EXPANDED_CARD_BOTTOM_INSET = + SCREEN_BOTTOM_INSET + + 24 + // Between bottom of sheet and bottom of Cancel/Confirm + 52 + // Cancel/Confirm height + 24 + // Between Cancel/Confirm and wallet avatar row + 44 + // Wallet avatar row height + 24; // Between wallet avatar row and bottom of expandable area + +export const COLLAPSED_CARD_HEIGHT = 56; +export const MAX_CARD_HEIGHT = 176; + +export const CARD_ROW_HEIGHT = 12; +export const SMALL_CARD_ROW_HEIGHT = 10; +export const CARD_BORDER_WIDTH = 1.5; + +export const EXPANDED_CARD_TOP_INSET = safeAreaInsetValues.top + 72; + +export const rotationConfig = { + duration: 2100, + easing: Easing.linear, +}; + +export const timingConfig = { + duration: 300, + easing: Easing.bezier(0.2, 0, 0, 1), +}; + +export const motiTimingConfig: TransitionConfig = { + duration: 225, + easing: Easing.bezier(0.2, 0, 0, 1), + type: 'timing', +}; + +export const SCREEN_FOR_REQUEST_SOURCE = { + [RequestSource.BROWSER]: Screens.DAPP_BROWSER, + [RequestSource.WALLETCONNECT]: Screens.WALLETCONNECT, + [RequestSource.MOBILE_WALLET_PROTOCOL]: Screens.MOBILE_WALLET_PROTOCOL, +}; + +export const CHARACTERS_PER_LINE = 40; +export const LINE_HEIGHT = 11; +export const LINE_GAP = 9; + +export const estimateMessageHeight = (message: string) => { + const estimatedLines = Math.ceil(message.length / CHARACTERS_PER_LINE); + const messageHeight = estimatedLines * LINE_HEIGHT + (estimatedLines - 1) * LINE_GAP + CARD_ROW_HEIGHT + 24 * 3 - CARD_BORDER_WIDTH * 2; + + return messageHeight; +}; + +export const infoForEventType: { [key: string]: EventInfo } = { + send: { + amountPrefix: '- ', + icon: '􀁷', + iconColor: 'red', + label: i18n.t(i18n.l.walletconnect.simulation.simulation_card.event_row.types.send), + textColor: 'red', + }, + receive: { + amountPrefix: '+ ', + icon: '􀁹', + iconColor: 'green', + label: i18n.t(i18n.l.walletconnect.simulation.simulation_card.event_row.types.receive), + textColor: 'green', + }, + approve: { + amountPrefix: '', + icon: '􀎤', + iconColor: 'green', + label: i18n.t(i18n.l.walletconnect.simulation.simulation_card.event_row.types.approve), + textColor: 'label', + }, + revoke: { + amountPrefix: '', + icon: '􀎠', + iconColor: 'red', + label: i18n.t(i18n.l.walletconnect.simulation.simulation_card.event_row.types.revoke), + textColor: 'label', + }, + failed: { + amountPrefix: '', + icon: '􀇿', + iconColor: 'red', + label: i18n.t(i18n.l.walletconnect.simulation.simulation_card.titles.likely_to_fail), + textColor: 'red', + }, + insufficientBalance: { + amountPrefix: '', + icon: '􀇿', + iconColor: 'blue', + label: '', + textColor: 'blue', + }, + MALICIOUS: { + amountPrefix: '', + icon: '􀇿', + iconColor: 'red', + label: '', + textColor: 'red', + }, + WARNING: { + amountPrefix: '', + icon: '􀇿', + iconColor: 'orange', + label: '', + textColor: 'orange', + }, +}; diff --git a/src/components/Transactions/types.ts b/src/components/Transactions/types.ts new file mode 100644 index 00000000000..ce9eaa8b3c3 --- /dev/null +++ b/src/components/Transactions/types.ts @@ -0,0 +1,18 @@ +import { TextColor } from '@/design-system/color/palettes'; + +export type EventType = 'send' | 'receive' | 'approve' | 'revoke' | 'failed' | 'insufficientBalance' | 'MALICIOUS' | 'WARNING'; + +export type EventInfo = { + amountPrefix: string; + icon: string; + iconColor: TextColor; + label: string; + textColor: TextColor; +}; + +export type DetailType = 'chain' | 'contract' | 'to' | 'function' | 'sourceCodeVerification' | 'dateCreated' | 'nonce'; + +export type DetailInfo = { + icon: string; + label: string; +}; diff --git a/src/components/activity-list/ActivityList.js b/src/components/activity-list/ActivityList.js index b57e625fe12..da1f88d403d 100644 --- a/src/components/activity-list/ActivityList.js +++ b/src/components/activity-list/ActivityList.js @@ -2,7 +2,6 @@ import * as lang from '@/languages'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { SectionList, StyleSheet, View } from 'react-native'; import sectionListGetItemLayout from 'react-native-section-list-get-item-layout'; -import networkTypes from '../../helpers/networkTypes'; import ActivityIndicator from '../ActivityIndicator'; import Spinner from '../Spinner'; import { ButtonPressAnimation } from '../animations'; @@ -14,6 +13,7 @@ import styled from '@/styled-thing'; import { useTheme } from '@/theme'; import { useSectionListScrollToTopContext } from '@/navigation/SectionListScrollToTopContext'; import { safeAreaInsetValues } from '@/utils'; +import { Network } from '@/networks/types'; const sx = StyleSheet.create({ sectionHeader: { @@ -106,7 +106,8 @@ const ActivityList = ({ if (!ref) return; setScrollToTopRef(ref); }; - if (network === networkTypes.mainnet) { + + if (network === Network.mainnet) { return ( remainingItemsLabel && } diff --git a/src/components/animations/AnimatedSpinner.tsx b/src/components/animations/AnimatedSpinner.tsx index 2931e958c55..94647785d39 100644 --- a/src/components/animations/AnimatedSpinner.tsx +++ b/src/components/animations/AnimatedSpinner.tsx @@ -68,7 +68,8 @@ export const AnimatedSpinner = ({ }); } } - } + }, + [] ); return ( diff --git a/src/components/asset-list/RecyclerAssetList2/FastComponents/FastBalanceCoinRow.tsx b/src/components/asset-list/RecyclerAssetList2/FastComponents/FastBalanceCoinRow.tsx index adb20230da8..f6a4885e514 100644 --- a/src/components/asset-list/RecyclerAssetList2/FastComponents/FastBalanceCoinRow.tsx +++ b/src/components/asset-list/RecyclerAssetList2/FastComponents/FastBalanceCoinRow.tsx @@ -12,6 +12,8 @@ import Routes from '@/navigation/routesNames'; import { borders, colors, padding, shadow } from '@/styles'; import RainbowCoinIcon from '@/components/coin-icon/RainbowCoinIcon'; import { ethereumUtils } from '@/utils'; +import { NativeCurrencyKey } from '@/entities'; +import { ChainId } from '@/networks/types'; interface CoinCheckButtonProps { isHidden: boolean; @@ -55,7 +57,7 @@ const formatPercentageString = (percentString?: string) => (percentString ? perc interface MemoizedBalanceCoinRowProps { uniqueId: string; - nativeCurrency: string; + nativeCurrency: NativeCurrencyKey; theme: any; navigate: any; nativeCurrencySymbol: string; @@ -65,7 +67,7 @@ interface MemoizedBalanceCoinRowProps { const MemoizedBalanceCoinRow = React.memo( ({ uniqueId, nativeCurrency, theme, navigate, nativeCurrencySymbol, isHidden, maybeCallback }: MemoizedBalanceCoinRowProps) => { - const item = useAccountAsset(uniqueId, nativeCurrency) as any; + const item = useAccountAsset(uniqueId, nativeCurrency); const handlePress = useCallback(() => { if (maybeCallback.current) { @@ -80,7 +82,7 @@ const MemoizedBalanceCoinRow = React.memo( } }, [navigate, item, maybeCallback]); - const percentChange = item?.native?.change; + const percentChange = item?.native?.change || undefined; const percentageChangeDisplay = formatPercentageString(percentChange); const isPositive = percentChange && percentageChangeDisplay.charAt(0) !== '-'; @@ -91,7 +93,7 @@ const MemoizedBalanceCoinRow = React.memo( const valueColor = nativeDisplay ? theme.colors.dark : theme.colors.blueGreyLight; - const chainId = ethereumUtils.getChainIdFromNetwork(item?.network); + const chainId = item?.chainId || ChainId.mainnet; return ( @@ -102,7 +104,7 @@ const MemoizedBalanceCoinRow = React.memo( size={40} icon={item?.icon_url} chainId={chainId} - symbol={item?.symbol} + symbol={item?.symbol || ''} theme={theme} colors={item?.colors} /> diff --git a/src/components/asset-list/RecyclerAssetList2/FastComponents/FastCoinBadge.tsx b/src/components/asset-list/RecyclerAssetList2/FastComponents/FastCoinBadge.tsx index 40262984048..01092ef5462 100644 --- a/src/components/asset-list/RecyclerAssetList2/FastComponents/FastCoinBadge.tsx +++ b/src/components/asset-list/RecyclerAssetList2/FastComponents/FastCoinBadge.tsx @@ -18,7 +18,7 @@ import BlastBadge from '@/assets/badges/blastBadge.png'; import BlastBadgeDark from '@/assets/badges/blastBadgeDark.png'; import DegenBadge from '@/assets/badges/degenBadge.png'; import DegenBadgeDark from '@/assets/badges/degenBadgeDark.png'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; interface FastChainBadgeProps { chainId: ChainId; diff --git a/src/components/asset-list/RecyclerAssetList2/FastComponents/FastCurrencySelectionRow.tsx b/src/components/asset-list/RecyclerAssetList2/FastComponents/FastCurrencySelectionRow.tsx index 15923fff565..0da7a18422e 100644 --- a/src/components/asset-list/RecyclerAssetList2/FastComponents/FastCurrencySelectionRow.tsx +++ b/src/components/asset-list/RecyclerAssetList2/FastComponents/FastCurrencySelectionRow.tsx @@ -13,7 +13,7 @@ import { colors, fonts, fontWithWidth, getFontSize } from '@/styles'; import { deviceUtils } from '@/utils'; import RainbowCoinIcon from '@/components/coin-icon/RainbowCoinIcon'; import { useExternalToken } from '@/resources/assets/externalAssetsQuery'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; const SafeRadialGradient = (IS_TESTING === 'true' ? View : RadialGradient) as typeof RadialGradient; diff --git a/src/components/asset-list/RecyclerAssetList2/NFTEmptyState.tsx b/src/components/asset-list/RecyclerAssetList2/NFTEmptyState.tsx index a23be055a48..26f131b8638 100644 --- a/src/components/asset-list/RecyclerAssetList2/NFTEmptyState.tsx +++ b/src/components/asset-list/RecyclerAssetList2/NFTEmptyState.tsx @@ -13,7 +13,6 @@ import { StyleSheet } from 'react-native'; import { LIGHT_SEPARATOR_COLOR, SEPARATOR_COLOR } from '@/__swaps__/screens/Swap/constants'; import { analyticsV2 } from '@/analytics'; import { convertRawAmountToRoundedDecimal } from '@/helpers/utilities'; -import { ethereumUtils } from '@/utils'; import { navigateToMintCollection } from '@/resources/reservoir/mints'; type LaunchFeaturedMintButtonProps = { @@ -32,8 +31,7 @@ const LaunchFeaturedMintButton = ({ featuredMint }: LaunchFeaturedMintButtonProp mintsLastHour: featuredMint.totalMints, priceInEth: convertRawAmountToRoundedDecimal(featuredMint.mintStatus.price, 18, 6), }); - const network = ethereumUtils.getNetworkFromChainId(featuredMint.chainId); - navigateToMintCollection(featuredMint.contract, featuredMint.mintStatus.price, network); + navigateToMintCollection(featuredMint.contract, featuredMint.mintStatus.price, featuredMint.chainId); } }, [featuredMint]); diff --git a/src/components/asset-list/RecyclerAssetList2/core/RawRecyclerList.tsx b/src/components/asset-list/RecyclerAssetList2/core/RawRecyclerList.tsx index 3f063c6a23d..f1ce574bb24 100644 --- a/src/components/asset-list/RecyclerAssetList2/core/RawRecyclerList.tsx +++ b/src/components/asset-list/RecyclerAssetList2/core/RawRecyclerList.tsx @@ -13,7 +13,7 @@ import rowRenderer from './RowRenderer'; import { BaseCellType, CellTypes, RecyclerListViewRef } from './ViewTypes'; import getLayoutProvider from './getLayoutProvider'; import useLayoutItemAnimator from './useLayoutItemAnimator'; -import { UniqueAsset } from '@/entities'; +import { NativeCurrencyKey, UniqueAsset } from '@/entities'; import { useRecyclerListViewScrollToTopContext } from '@/navigation/RecyclerListViewScrollToTopContext'; import { useAccountSettings, useCoinListEdited, useCoinListEditOptions, useWallets } from '@/hooks'; import { useNavigation } from '@/navigation'; @@ -29,7 +29,7 @@ const dataProvider = new DataProvider((r1, r2) => { export type ExtendedState = { theme: any; nativeCurrencySymbol: string; - nativeCurrency: string; + nativeCurrency: NativeCurrencyKey; navigate: any; isCoinListEdited: boolean; hiddenCoins: BooleanMap; diff --git a/src/components/asset-list/RecyclerAssetList2/profile-header/ProfileActionButtonsRow.tsx b/src/components/asset-list/RecyclerAssetList2/profile-header/ProfileActionButtonsRow.tsx index 2378b7c6f5d..9dd8a5ba71c 100644 --- a/src/components/asset-list/RecyclerAssetList2/profile-header/ProfileActionButtonsRow.tsx +++ b/src/components/asset-list/RecyclerAssetList2/profile-header/ProfileActionButtonsRow.tsx @@ -20,7 +20,7 @@ import { useAccountAccentColor } from '@/hooks/useAccountAccentColor'; import { addressCopiedToastAtom } from '@/recoil/addressCopiedToastAtom'; import { swapsStore } from '@/state/swaps/swapsStore'; import { userAssetsStore } from '@/state/assets/userAssets'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; export const ProfileActionButtonsRowHeight = 80; @@ -191,7 +191,7 @@ function SwapButton() { android && delayNext(); - const mainnetEth = await ethereumUtils.getNativeAssetForNetwork(ChainId.mainnet, accountAddress); + const mainnetEth = await ethereumUtils.getNativeAssetForNetwork({ chainId: ChainId.mainnet, address: accountAddress }); navigate(Routes.EXCHANGE_MODAL, { fromDiscover: true, params: { diff --git a/src/components/cards/EthCard.tsx b/src/components/cards/EthCard.tsx index 0e11d03b334..693ff97b0f8 100644 --- a/src/components/cards/EthCard.tsx +++ b/src/components/cards/EthCard.tsx @@ -22,10 +22,9 @@ import * as i18n from '@/languages'; import { ButtonPressAnimationTouchEvent } from '@/components/animations/ButtonPressAnimation/types'; import { useExternalToken } from '@/resources/assets/externalAssetsQuery'; import assetTypes from '@/entities/assetTypes'; -import { Network } from '@/networks/types'; +import { Network, ChainId } from '@/networks/types'; import { getUniqueId } from '@/utils/ethereumUtils'; import { EthCoinIcon } from '../coin-icon/EthCoinIcon'; -import { ChainId } from '@/__swaps__/types/chains'; export const ETH_CARD_HEIGHT = 284.3; @@ -45,6 +44,7 @@ export const EthCard = () => { ...externalEthAsset, address: ETH_ADDRESS, network: Network.mainnet, + chainId: ChainId.mainnet, uniqueId: getUniqueId(ETH_ADDRESS, ChainId.mainnet), }), [externalEthAsset] diff --git a/src/components/cards/FeaturedMintCard.tsx b/src/components/cards/FeaturedMintCard.tsx index 1b6e2e1cf12..b2738022f5c 100644 --- a/src/components/cards/FeaturedMintCard.tsx +++ b/src/components/cards/FeaturedMintCard.tsx @@ -27,7 +27,6 @@ import { Media } from '../Media'; import { analyticsV2 } from '@/analytics'; import * as i18n from '@/languages'; import { navigateToMintCollection } from '@/resources/reservoir/mints'; -import { ethereumUtils } from '@/utils'; const IMAGE_SIZE = 111; @@ -74,8 +73,7 @@ export function FeaturedMintCard() { mintsLastHour: featuredMint.totalMints, priceInEth: convertRawAmountToRoundedDecimal(featuredMint.mintStatus.price, 18, 6), }); - const network = ethereumUtils.getNetworkFromChainId(featuredMint.chainId); - navigateToMintCollection(featuredMint.contract, featuredMint.mintStatus.price, network); + navigateToMintCollection(featuredMint.contract, featuredMint.mintStatus.price, featuredMint.chainId); } }, [featuredMint]); diff --git a/src/components/cards/MintsCard/CollectionCell.tsx b/src/components/cards/MintsCard/CollectionCell.tsx index 01053b1a852..072057e5ce3 100644 --- a/src/components/cards/MintsCard/CollectionCell.tsx +++ b/src/components/cards/MintsCard/CollectionCell.tsx @@ -6,7 +6,7 @@ import { ButtonPressAnimation } from '@/components/animations'; import { useTheme } from '@/theme'; import { View } from 'react-native'; import { MintableCollection } from '@/graphql/__generated__/arc'; -import ethereumUtils, { useNativeAsset } from '@/utils/ethereumUtils'; +import { useNativeAsset } from '@/utils/ethereumUtils'; import { analyticsV2 } from '@/analytics'; import * as i18n from '@/languages'; import { IS_IOS } from '@/env'; @@ -58,8 +58,7 @@ export function CollectionCell({ collection }: { collection: MintableCollection priceInEth: amount, }); - const network = ethereumUtils.getNetworkFromChainId(collection.chainId); - navigateToMintCollection(collection.contract, collection.mintStatus.price, network); + navigateToMintCollection(collection.contract, collection.mintStatus.price, collection.chainId); }, [amount, collection.chainId, collection.contract, collection.contractAddress, collection.mintStatus.price]); return ( diff --git a/src/components/cards/NFTOffersCard/Offer.tsx b/src/components/cards/NFTOffersCard/Offer.tsx index dd2bcbc943f..b20da3382f8 100644 --- a/src/components/cards/NFTOffersCard/Offer.tsx +++ b/src/components/cards/NFTOffersCard/Offer.tsx @@ -23,6 +23,7 @@ import { useExternalToken } from '@/resources/assets/externalAssetsQuery'; import { useAccountSettings } from '@/hooks'; import { Network } from '@/networks/types'; import { ethereumUtils } from '@/utils'; +import { AddressOrEth } from '@/__swaps__/types/assets'; const TWO_HOURS_MS = 2 * 60 * 60 * 1000; export const CELL_HORIZONTAL_PADDING = 7; @@ -69,7 +70,7 @@ export const Offer = ({ offer }: { offer: NftOffer }) => { const { nativeCurrency } = useAccountSettings(); const offerChainId = ethereumUtils.getChainIdFromNetwork(offer.network as Network); const { data: externalAsset } = useExternalToken({ - address: offer.paymentToken.address, + address: offer.paymentToken.address as AddressOrEth, chainId: offerChainId, currency: nativeCurrency, }); diff --git a/src/components/cards/OpRewardsCard.tsx b/src/components/cards/OpRewardsCard.tsx index d53a421d187..89a307ff16e 100644 --- a/src/components/cards/OpRewardsCard.tsx +++ b/src/components/cards/OpRewardsCard.tsx @@ -8,6 +8,7 @@ import * as i18n from '@/languages'; import { useNavigation } from '@/navigation'; import Routes from '@/navigation/routesNames'; import { colors } from '@/styles'; +import { ChainId } from '@/networks/types'; const GRADIENT: Gradient = { colors: ['#520907', '#B22824'], @@ -23,7 +24,7 @@ export const OpRewardsCard: React.FC = () => { }; return ( - + onChangeAccount(wallet?.id, account.address), rowType: RowTypes.ADDRESS, walletId: wallet?.id, diff --git a/src/components/coin-icon/ChainBadge.js b/src/components/coin-icon/ChainBadge.js index 19cce6700b8..b5d25a2f67f 100644 --- a/src/components/coin-icon/ChainBadge.js +++ b/src/components/coin-icon/ChainBadge.js @@ -40,8 +40,7 @@ import { Centered } from '../layout'; import styled from '@/styled-thing'; import { position as positions } from '@/styles'; import { ChainBadgeSizeConfigs } from '@/components/coin-icon/ChainBadgeSizeConfigs'; -import { Network } from '@/networks/types'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; const ChainIcon = styled(FastImage)({ height: ({ containerSize }) => containerSize, diff --git a/src/components/coin-icon/ChainImage.tsx b/src/components/coin-icon/ChainImage.tsx index 844b96a2d18..1b2184ceb56 100644 --- a/src/components/coin-icon/ChainImage.tsx +++ b/src/components/coin-icon/ChainImage.tsx @@ -1,5 +1,5 @@ import React, { useMemo } from 'react'; -import { Network } from '@/helpers'; +import { ChainId } from '@/networks/types'; import ArbitrumBadge from '@/assets/badges/arbitrum.png'; import BaseBadge from '@/assets/badges/base.png'; @@ -12,7 +12,6 @@ import AvalancheBadge from '@/assets/badges/avalanche.png'; import BlastBadge from '@/assets/badges/blast.png'; import DegenBadge from '@/assets/badges/degen.png'; import FastImage, { Source } from 'react-native-fast-image'; -import { ChainId } from '@/__swaps__/types/chains'; export function ChainImage({ chainId, size = 20 }: { chainId: ChainId | null | undefined; size?: number }) { const source = useMemo(() => { diff --git a/src/components/coin-icon/EthCoinIcon.tsx b/src/components/coin-icon/EthCoinIcon.tsx index 4207a9e6333..53346a7afd0 100644 --- a/src/components/coin-icon/EthCoinIcon.tsx +++ b/src/components/coin-icon/EthCoinIcon.tsx @@ -3,7 +3,7 @@ import { useTheme } from '@/theme'; import { useNativeAsset } from '@/utils/ethereumUtils'; import RainbowCoinIcon from './RainbowCoinIcon'; import { ETH_SYMBOL } from '@/references'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; type EthCoinIconProps = { size?: number; diff --git a/src/components/coin-icon/RainbowCoinIcon.tsx b/src/components/coin-icon/RainbowCoinIcon.tsx index bcd086c3f39..d01e0e78b42 100644 --- a/src/components/coin-icon/RainbowCoinIcon.tsx +++ b/src/components/coin-icon/RainbowCoinIcon.tsx @@ -7,7 +7,7 @@ import { FallbackIcon as CoinIconTextFallback } from '@/utils'; import { FastFallbackCoinIconImage } from '../asset-list/RecyclerAssetList2/FastComponents/FastFallbackCoinIconImage'; import { FastChainBadge } from '../asset-list/RecyclerAssetList2/FastComponents/FastCoinBadge'; import { TokenColors } from '@/graphql/__generated__/metadata'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; const fallbackTextStyles = { fontFamily: fonts.family.SFProRounded, diff --git a/src/components/coin-icon/RequestVendorLogoIcon.js b/src/components/coin-icon/RequestVendorLogoIcon.js index d858f7522dd..cedf992c177 100644 --- a/src/components/coin-icon/RequestVendorLogoIcon.js +++ b/src/components/coin-icon/RequestVendorLogoIcon.js @@ -1,7 +1,7 @@ import React, { useMemo, useState } from 'react'; import { View } from 'react-native'; import { useTheme } from '../../theme/ThemeContext'; -import { ethereumUtils, initials } from '../../utils'; +import { initials } from '../../utils'; import ChainBadge from './ChainBadge'; import { Centered } from '../layout'; import { Text } from '../text'; @@ -33,7 +33,7 @@ export default function RequestVendorLogoIcon({ shouldPrioritizeImageLoading, showLargeShadow, size = CoinIconSize, - network, + chainId, ...props }) { const [error, setError] = useState(null); @@ -71,7 +71,7 @@ export default function RequestVendorLogoIcon({ )} - + ); } diff --git a/src/components/coin-icon/TwoCoinsIcon.tsx b/src/components/coin-icon/TwoCoinsIcon.tsx index a6537bb131c..d6827ad1942 100644 --- a/src/components/coin-icon/TwoCoinsIcon.tsx +++ b/src/components/coin-icon/TwoCoinsIcon.tsx @@ -4,7 +4,7 @@ import { ParsedAddressAsset } from '@/entities'; import { useTheme } from '@/theme'; import ChainBadge from './ChainBadge'; import RainbowCoinIcon from './RainbowCoinIcon'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; export function TwoCoinsIcon({ size = 45, diff --git a/src/components/coin-row/CoinRowInfoButton.js b/src/components/coin-row/CoinRowInfoButton.js index 63a58a9aa5b..59ad34022ad 100644 --- a/src/components/coin-row/CoinRowInfoButton.js +++ b/src/components/coin-row/CoinRowInfoButton.js @@ -93,7 +93,7 @@ const CoinRowInfoButton = ({ item, onCopySwapDetailsText, showFavoriteButton }) ); const onPressAndroid = useCallback(() => { - const blockExplorerText = `View on ${startCase(ethereumUtils.getBlockExplorer(ethereumUtils.getChainIdFromNetwork(item?.network)))}`; + const blockExplorerText = `View on ${startCase(ethereumUtils.getBlockExplorer(item?.chainId))}`; const androidContractActions = [lang.t('wallet.action.copy_contract_address'), blockExplorerText, lang.t('button.cancel')]; showActionSheetWithOptions( @@ -115,7 +115,7 @@ const CoinRowInfoButton = ({ item, onCopySwapDetailsText, showFavoriteButton }) }, [item, handleCopyContractAddress]); const menuConfig = useMemo(() => { - const blockExplorerAction = buildBlockExplorerAction(ethereumUtils.getChainIdFromNetwork(item?.network)); + const blockExplorerAction = buildBlockExplorerAction(item?.chainId); return { menuItems: [ blockExplorerAction, @@ -126,7 +126,7 @@ const CoinRowInfoButton = ({ item, onCopySwapDetailsText, showFavoriteButton }) ], menuTitle: `${item?.name} (${item?.symbol})`, }; - }, [item?.address, item?.name, item?.network, item?.symbol]); + }, [item?.address, item?.chainId, item?.name, item?.symbol]); const handlePressMenuItem = useCallback( ({ nativeEvent: { actionKey } }) => { diff --git a/src/components/coin-row/FastTransactionCoinRow.tsx b/src/components/coin-row/FastTransactionCoinRow.tsx index 9ee6cb3bb10..5adaba45b02 100644 --- a/src/components/coin-row/FastTransactionCoinRow.tsx +++ b/src/components/coin-row/FastTransactionCoinRow.tsx @@ -10,7 +10,7 @@ import Routes from '@rainbow-me/routes'; import { ImgixImage } from '../images'; import { CardSize } from '../unique-token/CardSize'; import { ChainBadge } from '../coin-icon'; -import { Network } from '@/networks/types'; +import { ChainId } from '@/networks/types'; import { address } from '@/utils/abbreviations'; import { TransactionType } from '@/resources/transactions/types'; import { @@ -26,7 +26,6 @@ import Spinner from '../Spinner'; import * as lang from '@/languages'; import RainbowCoinIcon from '../coin-icon/RainbowCoinIcon'; import { checkForPendingSwap } from '@/screens/transaction-details/helpers/checkForPendingSwap'; -import { ChainId } from '@/__swaps__/types/chains'; export const getApprovalLabel = ({ approvalAmount, asset, type }: Pick) => { if (!approvalAmount || !asset) return; @@ -408,7 +407,7 @@ export default React.memo(function TransactionCoinRow({ const [topValue] = activityValues(item, nativeCurrency) ?? []; return ( - + diff --git a/src/components/coin-row/SendCoinRow.js b/src/components/coin-row/SendCoinRow.js index 4795baafa14..8a0515f2422 100644 --- a/src/components/coin-row/SendCoinRow.js +++ b/src/components/coin-row/SendCoinRow.js @@ -3,7 +3,7 @@ import { TouchableWithoutFeedback } from 'react-native'; import LinearGradient from 'react-native-linear-gradient'; import { buildAssetUniqueIdentifier } from '../../helpers/assets'; import { useTheme } from '../../theme/ThemeContext'; -import { deviceUtils, ethereumUtils } from '../../utils'; +import { deviceUtils } from '../../utils'; import { ButtonPressAnimation } from '../animations'; import { Text } from '../text'; import CoinName from './CoinName'; @@ -105,9 +105,7 @@ const SendCoinRow = ({ const Wrapper = disablePressAnimation ? TouchableWithoutFeedback : ButtonPressAnimation; - const isL2 = useMemo(() => { - return isL2Chain({ chainId: ethereumUtils.getChainIdFromNetwork(item?.network) }); - }, [item?.network]); + const isL2 = useMemo(() => isL2Chain({ chainId: item?.chainId }), [item?.chainId]); const containerSelectedStyles = { height: selectedHeight, diff --git a/src/components/contacts/ContactRow.js b/src/components/contacts/ContactRow.js index b213d33dd23..e52b5f8251e 100644 --- a/src/components/contacts/ContactRow.js +++ b/src/components/contacts/ContactRow.js @@ -58,7 +58,7 @@ const ContactRow = ({ address, color, nickname, symmetricalMargins, ...props }, const { width: deviceWidth } = useDimensions(); const { onAddOrUpdateContacts } = useContacts(); const { colors } = useTheme(); - const { accountType, balances, ens, image, label, network, onPress, showcaseItem, testID } = props; + const { accountType, balances, ens, image, label, onPress, showcaseItem, testID } = props; const balanceText = balances ? balances.totalBalanceDisplay : i18n.t(i18n.l.wallet.change_wallet.loading_balance); @@ -80,12 +80,12 @@ const ContactRow = ({ address, color, nickname, symmetricalMargins, ...props }, const name = await fetchReverseRecord(address); if (name !== ensName) { setENSName(name); - onAddOrUpdateContacts(address, name && isENSAddressFormat(nickname) ? name : nickname, color, network, name); + onAddOrUpdateContacts(address, name && isENSAddressFormat(nickname) ? name : nickname, color, name); } }; fetchENSName(); } - }, [accountType, onAddOrUpdateContacts, address, color, ensName, network, nickname, profilesEnabled, setENSName]); + }, [accountType, onAddOrUpdateContacts, address, color, ensName, nickname, profilesEnabled, setENSName]); let cleanedUpLabel = null; if (label) { diff --git a/src/components/context-menu-buttons/ChainContextMenu.tsx b/src/components/context-menu-buttons/ChainContextMenu.tsx index 09828c27eb0..5f79b99c36e 100644 --- a/src/components/context-menu-buttons/ChainContextMenu.tsx +++ b/src/components/context-menu-buttons/ChainContextMenu.tsx @@ -3,7 +3,7 @@ import { ChainImage } from '@/components/coin-icon/ChainImage'; import { ContextMenuButton } from '@/components/context-menu'; import { Bleed, Box, Inline, Text, TextProps } from '@/design-system'; import * as i18n from '@/languages'; -import { ChainId, ChainNameDisplay } from '@/__swaps__/types/chains'; +import { ChainId, ChainNameDisplay } from '@/networks/types'; import { showActionSheetWithOptions } from '@/utils'; import { userAssetsStore } from '@/state/assets/userAssets'; import { chainNameForChainIdWithMainnetSubstitution } from '@/__swaps__/utils/chains'; diff --git a/src/components/discover/DiscoverSearchInput.js b/src/components/discover/DiscoverSearchInput.js index b7284d3c7f2..99913ebfef1 100644 --- a/src/components/discover/DiscoverSearchInput.js +++ b/src/components/discover/DiscoverSearchInput.js @@ -11,9 +11,9 @@ import { analytics } from '@/analytics'; import { ImgixImage } from '@/components/images'; import styled from '@/styled-thing'; import { margin, padding } from '@/styles'; -import { deviceUtils, ethereumUtils } from '@/utils'; +import { deviceUtils } from '@/utils'; import DiscoverSheetContext from '@/screens/discover/DiscoverScreenContext'; -import { getNetworkObj } from '@/networks'; +import { chainIdToNameMapping } from '@/networks/types'; export const ExchangeSearchHeight = 40; const ExchangeSearchWidth = deviceUtils.dimensions.width - 30; @@ -131,9 +131,8 @@ const ExchangeSearch = ( const placeholder = useMemo(() => { if (!currentChainId) return placeholderText; - const network = getNetworkObj(ethereumUtils.getNetworkFromChainId(currentChainId)); return lang.t('button.exchange_search_placeholder_network', { - network: network.name, + network: chainIdToNameMapping[currentChainId], }); }, [currentChainId, placeholderText]); diff --git a/src/components/ens-profile/ActionButtons/MoreButton.tsx b/src/components/ens-profile/ActionButtons/MoreButton.tsx index d22be293a06..dd695ea4e9e 100644 --- a/src/components/ens-profile/ActionButtons/MoreButton.tsx +++ b/src/components/ens-profile/ActionButtons/MoreButton.tsx @@ -12,7 +12,7 @@ import { RAINBOW_PROFILES_BASE_URL } from '@/references'; import Routes from '@/navigation/routesNames'; import { ethereumUtils } from '@/utils'; import { formatAddressForDisplay } from '@/utils/abbreviations'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; const ACTIONS = { ADD_CONTACT: 'add-contact', @@ -112,7 +112,7 @@ export default function MoreButton({ address, ensName }: { address?: string; ens setClipboard(address!); } if (address && actionKey === ACTIONS.ETHERSCAN) { - ethereumUtils.openAddressInBlockExplorer(address, ChainId.mainnet); + ethereumUtils.openAddressInBlockExplorer({ address, chainId: ChainId.mainnet }); } if (actionKey === ACTIONS.ADD_CONTACT) { navigate(Routes.MODAL_SCREEN, { diff --git a/src/components/exchange/ConfirmExchangeButton.js b/src/components/exchange/ConfirmExchangeButton.js index 05b438e0acd..455517866ee 100644 --- a/src/components/exchange/ConfirmExchangeButton.js +++ b/src/components/exchange/ConfirmExchangeButton.js @@ -14,12 +14,12 @@ import Routes from '@/navigation/routesNames'; import { lightModeThemeColors } from '@/styles'; import { useTheme } from '@/theme'; import handleSwapErrorCodes from '@/utils/exchangeErrorCodes'; -import { getNetworkObj } from '@/networks'; +import { getNetworkObject } from '@/networks'; const NOOP = () => null; export default function ConfirmExchangeButton({ - currentNetwork, + chainId, disabled, loading, isHighPriceImpact, @@ -95,7 +95,7 @@ export default function ConfirmExchangeButton({ label = lang.t('button.confirm_exchange.insufficient_funds'); } else if (isSufficientGas != null && !isSufficientGas) { label = lang.t('button.confirm_exchange.insufficient_token', { - tokenName: getNetworkObj(currentNetwork).nativeCurrency.symbol, + tokenName: getNetworkObject({ chainId }).nativeCurrency.symbol, }); } else if (!isValidGas && isGasReady) { label = lang.t('button.confirm_exchange.invalid_fee'); diff --git a/src/components/exchange/ExchangeField.tsx b/src/components/exchange/ExchangeField.tsx index a31dc8e749b..f72ed6fe138 100644 --- a/src/components/exchange/ExchangeField.tsx +++ b/src/components/exchange/ExchangeField.tsx @@ -4,14 +4,13 @@ import { TokenSelectionButton } from '../buttons'; import { ChainBadge, CoinIconSize } from '../coin-icon'; import { EnDash } from '../text'; import ExchangeInput from './ExchangeInput'; -import { Network } from '@/helpers'; +import { ChainId } from '@/networks/types'; import styled from '@/styled-thing'; import { borders } from '@/styles'; import { useTheme } from '@/theme'; import { AccentColorProvider, Box, Space } from '@/design-system'; import RainbowCoinIcon from '../coin-icon/RainbowCoinIcon'; import { TokenColors } from '@/graphql/__generated__/metadata'; -import { ChainId } from '@/__swaps__/types/chains'; const ExchangeFieldHeight = android ? 64 : 38; const ExchangeFieldPadding: Space = android ? '15px (Deprecated)' : '19px (Deprecated)'; diff --git a/src/components/exchange/ExchangeInputField.tsx b/src/components/exchange/ExchangeInputField.tsx index 490cc202a04..438955417c8 100644 --- a/src/components/exchange/ExchangeInputField.tsx +++ b/src/components/exchange/ExchangeInputField.tsx @@ -4,10 +4,9 @@ import { ColumnWithMargins, Row } from '../layout'; import ExchangeField from './ExchangeField'; import ExchangeMaxButton from './ExchangeMaxButton'; import ExchangeNativeField from './ExchangeNativeField'; -import { Network } from '@/helpers'; +import { ChainId } from '@/networks/types'; import styled from '@/styled-thing'; import { TokenColors } from '@/graphql/__generated__/metadata'; -import { ChainId } from '@/__swaps__/types/chains'; const Container = styled(ColumnWithMargins).attrs({ margin: 5 })({ paddingTop: android ? 0 : 6, diff --git a/src/components/exchange/ExchangeOutputField.tsx b/src/components/exchange/ExchangeOutputField.tsx index 479862c16f6..0ada42075e3 100644 --- a/src/components/exchange/ExchangeOutputField.tsx +++ b/src/components/exchange/ExchangeOutputField.tsx @@ -3,7 +3,7 @@ import { TextInput } from 'react-native'; import ExchangeField from './ExchangeField'; import { Box } from '@rainbow-me/design-system'; import { TokenColors } from '@/graphql/__generated__/metadata'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; interface ExchangeOutputFieldProps { color: string; diff --git a/src/components/exchange/ExchangeTokenRow.tsx b/src/components/exchange/ExchangeTokenRow.tsx index b1280c79fcc..5a1f9254bf5 100644 --- a/src/components/exchange/ExchangeTokenRow.tsx +++ b/src/components/exchange/ExchangeTokenRow.tsx @@ -9,7 +9,8 @@ import { IS_IOS } from '@/env'; import { FavStar, Info } from '../asset-list/RecyclerAssetList2/FastComponents/FastCurrencySelectionRow'; import { View } from 'react-native'; import RainbowCoinIcon from '../coin-icon/RainbowCoinIcon'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; +import { ParsedAddressAsset } from '@/entities'; interface ExchangeTokenRowProps { item: any; @@ -48,7 +49,6 @@ export default React.memo(function ExchangeTokenRow({ {name ?? item?.name} - {showBalance && item?.balance?.display && ( + {showBalance && (item as ParsedAddressAsset)?.balance?.display && ( - {item?.balance?.display ?? ''} + {(item as ParsedAddressAsset)?.balance?.display ?? ''} )} {!showBalance && ( @@ -92,7 +92,7 @@ export default React.memo(function ExchangeTokenRow({ {showBalance && ( - {item?.native?.balance?.display ?? `${nativeCurrencySymbol}0.00`} + {(item as ParsedAddressAsset)?.native?.balance?.display ?? `${nativeCurrencySymbol}0.00`} )} diff --git a/src/components/exchange/NetworkSwitcher.js b/src/components/exchange/NetworkSwitcher.js index 44870fb6286..e9b3a1812d7 100644 --- a/src/components/exchange/NetworkSwitcher.js +++ b/src/components/exchange/NetworkSwitcher.js @@ -8,11 +8,12 @@ import { Column, Row } from '../layout'; import { Text } from '../text'; import { padding, position } from '@/styles'; import { ethereumUtils, showActionSheetWithOptions } from '@/utils'; -import { RainbowNetworks, getNetworkObj } from '@/networks'; +import { RainbowNetworkObjects } from '@/networks'; import { EthCoinIcon } from '../coin-icon/EthCoinIcon'; +import { chainIdToNameMapping } from '@/networks/types'; const networkMenuItems = () => { - return RainbowNetworks.filter(network => network.features.swaps).map(network => ({ + return RainbowNetworkObjects.filter(network => network.features.swaps).map(network => ({ actionKey: network.value, actionTitle: network.name, icon: { @@ -22,7 +23,7 @@ const networkMenuItems = () => { })); }; const androidNetworkMenuItems = () => { - return RainbowNetworks.filter(network => network.features.swaps).map(network => network.name); + return RainbowNetworkObjects.filter(network => network.features.swaps).map(network => network.name); }; const NetworkSwitcherv1 = ({ @@ -93,7 +94,7 @@ const NetworkSwitcherv1 = ({ weight={prominent ? 'heavy' : 'bold'} > {lang.t('expanded_state.swap.network_switcher', { - network: getNetworkObj(ethereumUtils.getNetworkFromChainId(currentChainId)).name, + network: chainIdToNameMapping[currentChainId], })} diff --git a/src/components/exchange/NetworkSwitcherv2.tsx b/src/components/exchange/NetworkSwitcherv2.tsx index 2ee33c53529..a16693c7cbb 100644 --- a/src/components/exchange/NetworkSwitcherv2.tsx +++ b/src/components/exchange/NetworkSwitcherv2.tsx @@ -4,11 +4,11 @@ import RadialGradient from 'react-native-radial-gradient'; import { ButtonPressAnimation } from '../animations'; import ChainBadge from '../coin-icon/ChainBadge'; import { Bleed, Box, Columns, Inline, Text } from '@/design-system'; -import { Network } from '@/helpers'; import { position } from '@rainbow-me/styles'; import { useTheme } from '@/theme'; import { sortNetworks } from '@/networks'; import { EthCoinIcon } from '../coin-icon/EthCoinIcon'; +import { ChainId } from '@/networks/types'; const NetworkSwitcherv2 = ({ currentChainId, @@ -31,10 +31,10 @@ const NetworkSwitcherv2 = ({ })); }, []); - const radialGradientProps = (network: Network) => { + const radialGradientProps = (chainId: ChainId) => { return { center: [0, 1], - colors: [colors.alpha(colors.networkColors[network], 0.1), colors.alpha(colors.networkColors[network], 0.02)], + colors: [colors.alpha(colors.networkColors[chainId], 0.1), colors.alpha(colors.networkColors[chainId], 0.02)], pointerEvents: 'none', style: { ...position.coverAsObject, @@ -55,7 +55,7 @@ const NetworkSwitcherv2 = ({ testID={'network-switcher-scroll-view'} > - {networkMenuItems.map(({ chainId, title, network }) => { + {networkMenuItems.map(({ chainId, title }) => { const isSelected = currentChainId === chainId; return ( setCurrentChainId(chainId)} padding="8px" - testID={`${testID}-${network}`} + testID={`${testID}-${chainId}`} > {isSelected && ( )} - {network === Network.mainnet ? ( + {chainId === ChainId.mainnet ? ( ) : ( )} {title} diff --git a/src/components/exchange/exchangeAssetRowContextMenuProps.ts b/src/components/exchange/exchangeAssetRowContextMenuProps.ts index 57d0c2dc12f..e1de9563887 100644 --- a/src/components/exchange/exchangeAssetRowContextMenuProps.ts +++ b/src/components/exchange/exchangeAssetRowContextMenuProps.ts @@ -3,11 +3,11 @@ import { startCase } from 'lodash'; import { NativeSyntheticEvent } from 'react-native'; import { setClipboard } from '../../hooks/useClipboard'; import { abbreviations, ethereumUtils, haptics, showActionSheetWithOptions } from '@/utils'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; const buildBlockExplorerAction = (chainId: ChainId) => { const blockExplorerText = lang.t('exchange.coin_row.view_on', { - blockExplorerName: startCase(ethereumUtils.getBlockExplorer(chainId)), + blockExplorerName: startCase(ethereumUtils.getBlockExplorer({ chainId })), }); return { actionKey: CoinRowActionsEnum.blockExplorer, @@ -43,7 +43,7 @@ export default function contextMenuProps(item: any, onCopySwapDetailsText: (addr }; const onPressAndroid = () => { - const blockExplorerText = `View on ${startCase(ethereumUtils.getBlockExplorer(ethereumUtils.getChainIdFromNetwork(item?.network)))}`; + const blockExplorerText = `View on ${startCase(ethereumUtils.getBlockExplorer({ chainId: ethereumUtils.getChainIdFromNetwork(item?.network) }))}`; const androidContractActions = [lang.t('wallet.action.copy_contract_address'), blockExplorerText, lang.t('button.cancel')]; showActionSheetWithOptions( diff --git a/src/components/expanded-state/AvailableNetworks.js b/src/components/expanded-state/AvailableNetworks.js index 72cba5b9315..f6e96c439ab 100644 --- a/src/components/expanded-state/AvailableNetworks.js +++ b/src/components/expanded-state/AvailableNetworks.js @@ -3,11 +3,9 @@ import React from 'react'; import { Linking } from 'react-native'; import RadialGradient from 'react-native-radial-gradient'; import { Box } from '@/design-system'; -import networkInfo from '@/helpers/networkInfo'; import { useNavigation } from '@/navigation'; import Routes from '@/navigation/routesNames'; import { padding, position } from '@/styles'; -import { ethereumUtils } from '@/utils'; import { useTheme } from '@/theme'; import { ButtonPressAnimation } from '../animations'; import { Column, Row } from '../layout'; @@ -15,11 +13,11 @@ import { ChainBadge } from '../coin-icon'; import Divider from '../Divider'; import { Text } from '../text'; import { EthCoinIcon } from '../coin-icon/EthCoinIcon'; +import { ChainId, chainIdToNameMapping } from '@/networks/types'; const AvailableNetworksv1 = ({ asset, networks, hideDivider, marginBottom = 24, marginHorizontal = 19, prominent }) => { const { colors } = useTheme(); const { navigate } = useNavigation(); - const radialGradientProps = { center: [0, 1], colors: colors.gradients.lightGreyWhite, @@ -30,9 +28,7 @@ const AvailableNetworksv1 = ({ asset, networks, hideDivider, marginBottom = 24, }, }; - const availableNetworks = Object.keys(networks).map(network => { - return ethereumUtils.getNetworkFromChainId(Number(network)); - }); + const availableChainIds = Object.keys(networks).map(network => Number(network)); const linkToHop = useCallback(() => { Linking.openURL('https://app.hop.exchange/#/send'); @@ -40,12 +36,12 @@ const AvailableNetworksv1 = ({ asset, networks, hideDivider, marginBottom = 24, const handleAvailableNetworksPress = useCallback(() => { navigate(Routes.EXPLAIN_SHEET, { - networks: availableNetworks, + chainIds: availableChainIds, onClose: linkToHop, tokenSymbol: asset.symbol, type: 'availableNetworks', }); - }, [navigate, availableNetworks, linkToHop, asset.symbol]); + }, [navigate, availableChainIds, linkToHop, asset.symbol]); return ( <> @@ -53,12 +49,12 @@ const AvailableNetworksv1 = ({ asset, networks, hideDivider, marginBottom = 24, - {availableNetworks?.map((network, index) => { + {availableChainIds?.map((chainId, index) => { return ( - {network !== 'mainnet' ? ( - + {chainId !== ChainId.mainnet ? ( + ) : ( )} @@ -85,12 +81,12 @@ const AvailableNetworksv1 = ({ asset, networks, hideDivider, marginBottom = 24, size="smedium" weight={prominent ? 'heavy' : 'bold'} > - {availableNetworks?.length > 1 + {availableChainIds?.length > 1 ? lang.t('expanded_state.asset.available_networks', { - availableNetworks: availableNetworks?.length, + availableNetworks: availableChainIds?.length, }) : lang.t('expanded_state.asset.available_network', { - availableNetwork: networkInfo[availableNetworks?.[0]]?.name, + availableNetwork: chainIdToNameMapping[availableChainIds[0]]?.name, })} diff --git a/src/components/expanded-state/AvailableNetworksv2.tsx b/src/components/expanded-state/AvailableNetworksv2.tsx index 496a8bacd3a..b1345505800 100644 --- a/src/components/expanded-state/AvailableNetworksv2.tsx +++ b/src/components/expanded-state/AvailableNetworksv2.tsx @@ -8,14 +8,14 @@ import { useNavigation } from '@/navigation'; import Routes from '@/navigation/routesNames'; import { position } from '@/styles'; import { ethereumUtils, watchingAlert } from '@/utils'; -import { CurrencySelectionTypes, ExchangeModalTypes, Network } from '@/helpers'; +import { CurrencySelectionTypes, ExchangeModalTypes } from '@/helpers'; import { useSwapCurrencyHandlers, useWallets } from '@/hooks'; import { RainbowToken } from '@/entities'; import { useTheme } from '@/theme'; import { ButtonPressAnimation } from '../animations'; import ContextMenuButton from '@/components/native-context-menu/contextMenu'; import { implementation } from '@/entities/dispersion'; -import { RainbowNetworks, getNetworkObj } from '@/networks'; +import { RainbowNetworkObjects } from '@/networks'; import { EthCoinIcon } from '../coin-icon/EthCoinIcon'; import { SWAPS_V2, enableActionsOnReadOnlyWallet, useExperimentalFlag } from '@/config'; import { useRemoteConfig } from '@/model/remoteConfig'; @@ -25,7 +25,8 @@ import { AddressOrEth, AssetType } from '@/__swaps__/types/assets'; import { chainNameFromChainId } from '@/__swaps__/utils/chains'; import { swapsStore } from '@/state/swaps/swapsStore'; import { InteractionManager } from 'react-native'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId, chainIdToNameMapping } from '@/networks/types'; +import { getUniqueId } from '@/utils/ethereumUtils'; const NOOP = () => null; @@ -56,11 +57,11 @@ const AvailableNetworksv2 = ({ }, }; - const availableNetworks = useMemo(() => { + const availableChainIds = useMemo(() => { // we dont want to show mainnet return Object.keys(networks) - .map(network => ethereumUtils.getNetworkFromChainId(Number(network))) - .filter(network => network !== Network.mainnet); + .filter(chainId => Number(chainId) !== ChainId.mainnet) + .map(chainId => Number(chainId)); }, [networks]); const { updateInputCurrency } = useSwapCurrencyHandlers({ @@ -68,7 +69,7 @@ const AvailableNetworksv2 = ({ type: ExchangeModalTypes.swap, }); const convertAssetAndNavigate = useCallback( - (chosenNetwork: Network) => { + (chainId: ChainId) => { if (isReadOnlyWallet && !enableActionsOnReadOnlyWallet) { watchingAlert(); return; @@ -77,15 +78,14 @@ const AvailableNetworksv2 = ({ const newAsset = asset; // we need to convert the mainnet asset to the selected network's - newAsset.mainnet_address = networks?.[ethereumUtils.getChainIdFromNetwork(Network.mainnet)]?.address ?? asset.address; - newAsset.address = networks?.[ethereumUtils.getChainIdFromNetwork(chosenNetwork)].address; - newAsset.network = chosenNetwork; + newAsset.mainnet_address = networks?.[ChainId.mainnet]?.address ?? asset.address; + newAsset.address = networks?.[chainId].address; + newAsset.chainId = chainId; goBack(); if (swapsV2Enabled || swaps_v2) { - const chainId = ethereumUtils.getChainIdFromNetwork(newAsset.network); - const uniqueId = `${newAsset.address}_${chainId}`; + const uniqueId = `${newAsset.address}_${asset.chainId}`; const userAsset = userAssetsStore.getState().userAssets.get(uniqueId); const parsedAsset = parseSearchAsset({ @@ -94,16 +94,16 @@ const AvailableNetworksv2 = ({ uniqueId, address: newAsset.address as AddressOrEth, type: newAsset.type as AssetType, - chainId, - chainName: chainNameFromChainId(chainId), + chainId: asset.chainId, + chainName: chainNameFromChainId(asset.chainId), isNativeAsset: false, native: {}, }, searchAsset: { ...newAsset, uniqueId, - chainId, - chainName: chainNameFromChainId(chainId), + chainId: asset.chainId, + chainName: chainNameFromChainId(asset.chainId), address: newAsset.address as AddressOrEth, highLiquidity: newAsset.highLiquidity ?? false, isRainbowCurated: newAsset.isRainbowCurated ?? false, @@ -118,7 +118,7 @@ const AvailableNetworksv2 = ({ const largestBalanceSameChainUserAsset = userAssetsStore .getState() .getUserAssets() - .find(userAsset => userAsset.chainId === chainId && userAsset.address !== newAsset.address); + .find(userAsset => userAsset.chainId === asset.chainId && userAsset.address !== newAsset.address); if (largestBalanceSameChainUserAsset) { swapsStore.setState({ inputAsset: largestBalanceSameChainUserAsset }); } else { @@ -133,8 +133,8 @@ const AvailableNetworksv2 = ({ return; } - newAsset.uniqueId = `${asset.address}_${chosenNetwork}`; - newAsset.type = chosenNetwork; + newAsset.uniqueId = getUniqueId(asset.address, chainId); + newAsset.type = ethereumUtils.getNetworkFromChainId(chainId); navigate(Routes.EXCHANGE_MODAL, { params: { @@ -151,37 +151,35 @@ const AvailableNetworksv2 = ({ screen: Routes.CURRENCY_SELECT_SCREEN, }); }, - [asset, goBack, navigate, networks, swapsV2Enabled, swaps_v2, updateInputCurrency] + [asset, goBack, isReadOnlyWallet, navigate, networks, swapsV2Enabled, swaps_v2, updateInputCurrency] ); const handlePressContextMenu = useCallback( // @ts-expect-error ContextMenu is an untyped JS component and can't type its onPress handler properly - ({ nativeEvent: { actionKey: network } }) => { - convertAssetAndNavigate(network); + ({ nativeEvent: { actionKey: chainId } }) => { + convertAssetAndNavigate(chainId); }, [convertAssetAndNavigate] ); const handlePressButton = useCallback(() => { - convertAssetAndNavigate(availableNetworks[0]); - }, [availableNetworks, convertAssetAndNavigate]); + convertAssetAndNavigate(availableChainIds[0]); + }, [availableChainIds, convertAssetAndNavigate]); const networkMenuItems = useMemo(() => { - return RainbowNetworks.filter(({ features, value, id }) => features.swaps && value !== Network.mainnet && !!networks[id]).map( - network => ({ - actionKey: network.value, - actionTitle: network.name, - icon: { - iconType: 'ASSET', - iconValue: `${network.networkType === 'layer2' ? `${network.value}BadgeNoShadow` : 'ethereumBadge'}`, - }, - }) - ); + return RainbowNetworkObjects.filter(({ features, id }) => features.swaps && id !== ChainId.mainnet && !!networks[id]).map(network => ({ + actionKey: `${network.id}`, + actionTitle: network.name, + icon: { + iconType: 'ASSET', + iconValue: `${network.networkType === 'layer2' ? `${network.value}BadgeNoShadow` : 'ethereumBadge'}`, + }, + })); }, [networks]); - const MenuWrapper = availableNetworks.length > 1 ? ContextMenuButton : Box; + const MenuWrapper = availableChainIds.length > 1 ? ContextMenuButton : Box; - if (availableNetworks.length === 0) return null; + if (availableChainIds.length === 0) return null; return ( <> @@ -208,16 +206,15 @@ const AvailableNetworksv2 = ({ - {availableNetworks?.map((network, index) => { - const chainId = ethereumUtils.getChainIdFromNetwork(network); + {availableChainIds?.map((chainId, index) => { return ( @@ -233,18 +230,18 @@ const AvailableNetworksv2 = ({ - {availableNetworks?.length > 1 + {availableChainIds?.length > 1 ? lang.t('expanded_state.asset.available_networks', { - availableNetworks: availableNetworks?.length, + availableNetworks: availableChainIds?.length, }) : lang.t('expanded_state.asset.available_networkv2', { - availableNetwork: getNetworkObj(availableNetworks?.[0])?.name, + availableNetwork: chainIdToNameMapping[availableChainIds[0]], })} - {availableNetworks?.length > 1 ? '􀁱' : '􀯻'} + {availableChainIds?.length > 1 ? '􀁱' : '􀯻'} diff --git a/src/components/expanded-state/ContactProfileState.js b/src/components/expanded-state/ContactProfileState.js index b6bde10d0de..9a895e6dedc 100644 --- a/src/components/expanded-state/ContactProfileState.js +++ b/src/components/expanded-state/ContactProfileState.js @@ -7,7 +7,7 @@ import { magicMemo } from '../../utils'; import ProfileModal from './profile/ProfileModal'; import useExperimentalFlag, { PROFILES } from '@/config/experimentalHooks'; import { removeFirstEmojiFromString, returnStringFirstEmoji } from '@/helpers/emojiHandler'; -import { useAccountSettings, useContacts, useENSAvatar } from '@/hooks'; +import { useContacts, useENSAvatar } from '@/hooks'; import { addressHashedColorIndex, addressHashedEmoji } from '@/utils/profileUtils'; import { usePersistentDominantColorFromImage } from '@/hooks/usePersistentDominantColorFromImage'; @@ -24,16 +24,14 @@ const ContactProfileState = ({ address, color, contact, ens, nickname }) => { const colorIndex = useMemo(() => color || addressHashedColorIndex(address) || 0, [address, color]); - const { network } = useAccountSettings(); - const handleAddContact = useCallback(() => { const nickname = profilesEnabled ? value : (emoji ? `${emoji} ${value}` : value).trim(); if (value?.length > 0) { - onAddOrUpdateContacts(address, nickname, colors.avatarBackgrounds[colorIndex || 0], network, ens); + onAddOrUpdateContacts(address, nickname, colors.avatarBackgrounds[colorIndex || 0], ens); goBack(); } android && Keyboard.dismiss(); - }, [address, colorIndex, colors.avatarBackgrounds, emoji, ens, goBack, network, onAddOrUpdateContacts, profilesEnabled, value]); + }, [address, colorIndex, colors.avatarBackgrounds, emoji, ens, goBack, onAddOrUpdateContacts, profilesEnabled, value]); const handleCancel = useCallback(() => { goBack(); diff --git a/src/components/expanded-state/CustomGasState.js b/src/components/expanded-state/CustomGasState.js index eed7e54ba95..d19af128e10 100644 --- a/src/components/expanded-state/CustomGasState.js +++ b/src/components/expanded-state/CustomGasState.js @@ -40,7 +40,7 @@ export default function CustomGasState({ asset }) { const { height: deviceHeight } = useDimensions(); const keyboardHeight = useKeyboardHeight(); const colorForAsset = useColorForAsset(asset || {}, fallbackColor, false, true); - const { selectedGasFee, currentBlockParams, txNetwork } = useGas(); + const { selectedGasFee, currentBlockParams, chainId } = useGas(); const [canGoBack, setCanGoBack] = useState(true); const { tradeDetails } = useSelector(state => state.swap); @@ -93,7 +93,7 @@ export default function CustomGasState({ asset }) { { - switch (network) { - case Network.mainnet: +const getIsSupportedOnRainbowWeb = (chainId: ChainId) => { + switch (chainId) { + case ChainId.mainnet: return true; default: return false; @@ -251,7 +251,7 @@ const UniqueTokenExpandedState = ({ asset: passedAsset, external }: UniqueTokenE [offer] ); - const isSupportedOnRainbowWeb = getIsSupportedOnRainbowWeb(asset.network); + const isSupportedOnRainbowWeb = getIsSupportedOnRainbowWeb(asset.chainId); const [isRefreshMetadataToastActive, setIsRefreshMetadataToastActive] = useState(false); const [isReportSpamToastActive, setIsReportSpamToastActive] = useState(false); @@ -548,10 +548,10 @@ const UniqueTokenExpandedState = ({ asset: passedAsset, external }: UniqueTokenE /> )} - {asset.network !== Network.mainnet ? ( + {asset.chainId !== ChainId.mainnet ? ( // @ts-expect-error JavaScript component isL2Chain({ chainId: assetChainId }), [assetChainId]); - const isTestnet = isTestnetNetwork(currentNetwork); + const isL2 = useMemo(() => isL2Chain({ chainId: asset?.chainId }), [asset?.chainId]); + const isTestnet = isTestnetChain({ chainId: currentChainId }); const { data, isLoading: additionalAssetDataLoading } = useAdditionalAssetData({ address: asset?.address, - network: asset?.network, + chainId: asset?.chainId, currency: nativeCurrency, }); @@ -235,12 +235,13 @@ export default function ChartExpandedState({ asset }) { const { colors } = useTheme(); const crosschainEnabled = useExperimentalFlag(CROSSCHAIN_SWAPS); + const AvailableNetworks = !crosschainEnabled ? AvailableNetworksv1 : AvailableNetworksv2; - const assetNetwork = assetWithPrice.network; + const assetChainId = assetWithPrice.chainId; const { swagg_enabled, f2c_enabled } = useRemoteConfig(); - const swapEnabled = swagg_enabled && getNetworkObj(assetNetwork).features.swaps; + const swapEnabled = swagg_enabled && getNetworkObject({ chainId: assetChainId }).features.swaps; const addCashEnabled = f2c_enabled; const format = useCallback( @@ -314,7 +315,7 @@ export default function ChartExpandedState({ asset }) { ) : null} {!data?.networks && isL2 && ( - + )} {data?.networks && !hasBalance && ( @@ -375,7 +376,7 @@ export default function ChartExpandedState({ asset }) { isNativeAsset={assetWithPrice?.isNativeAsset} links={data?.links} marginTop={!delayedDescriptions && 19} - chainId={ethereumUtils.getChainIdFromNetwork(asset?.network)} + chainId={asset?.chainId} /> diff --git a/src/components/expanded-state/asset/SocialLinks.js b/src/components/expanded-state/asset/SocialLinks.js index 79d2706a75a..f5199f6480d 100644 --- a/src/components/expanded-state/asset/SocialLinks.js +++ b/src/components/expanded-state/asset/SocialLinks.js @@ -28,8 +28,8 @@ const CommunityLink = styled(Link).attrs({ }); export default function SocialLinks({ address, color, isNativeAsset, links, marginTop, chainId }) { - const etherscanURL = ethereumUtils.getEtherscanHostForNetwork(chainId); - const blockExplorerName = ethereumUtils.getBlockExplorer(chainId); + const etherscanURL = ethereumUtils.getEtherscanHostForNetwork({ chainId }); + const blockExplorerName = ethereumUtils.getBlockExplorer({ chainId }); return ( <> diff --git a/src/components/expanded-state/chart/ChartContextButton.js b/src/components/expanded-state/chart/ChartContextButton.js index 8ce84794e84..19044bc6fe1 100644 --- a/src/components/expanded-state/chart/ChartContextButton.js +++ b/src/components/expanded-state/chart/ChartContextButton.js @@ -46,12 +46,12 @@ export default function ChartContextButton({ asset, color }) { ? [] : [ `🔍 ${emojiSpacing}${lang.t('wallet.action.view_on', { - blockExplorerName: startCase(ethereumUtils.getBlockExplorer(ethereumUtils.getChainIdFromNetwork(asset?.network))), + blockExplorerName: startCase(ethereumUtils.getBlockExplorer({ chainId: asset?.chainId })), })}`, ]), ...(ios ? [lang.t('wallet.action.cancel')] : []), ], - [asset?.isNativeAsset, asset?.network, currentAction] + [asset?.chainId, asset?.isNativeAsset, currentAction] ); return ; diff --git a/src/components/expanded-state/chart/ChartExpandedStateHeader.js b/src/components/expanded-state/chart/ChartExpandedStateHeader.js index 624255db57e..2d23e3ca37c 100644 --- a/src/components/expanded-state/chart/ChartExpandedStateHeader.js +++ b/src/components/expanded-state/chart/ChartExpandedStateHeader.js @@ -11,7 +11,6 @@ import { useAccountSettings, useBooleanState } from '@/hooks'; import styled from '@/styled-thing'; import { padding } from '@/styles'; import RainbowCoinIcon from '@/components/coin-icon/RainbowCoinIcon'; -import { ethereumUtils } from '@/utils'; const noPriceData = lang.t('expanded_state.chart.no_price_data'); @@ -52,9 +51,6 @@ export default function ChartExpandedStateHeader({ }) { const theme = useTheme(); const color = givenColors || theme.colors.dark; - const tokens = useMemo(() => { - return isPool ? asset.tokens : [asset]; - }, [asset, isPool]); const { nativeCurrency } = useAccountSettings(); const tabularNums = useTabularNumsWhileScrubbing(); @@ -114,7 +110,7 @@ export default function ChartExpandedStateHeader({ (null); const [maxPriorityFeeError, setMaxPriorityFeeError] = useState(null); @@ -248,12 +248,12 @@ export default function FeesPanel({ currentGasTrend, colorForAsset, setCanGoBack ); const addMinerTip = useCallback(() => { - updatePriorityFeePerGas(calculateMinerTipAddDifference(maxPriorityFee, txNetwork)); - }, [maxPriorityFee, txNetwork, updatePriorityFeePerGas]); + updatePriorityFeePerGas(calculateMinerTipAddDifference(maxPriorityFee, chainId)); + }, [maxPriorityFee, chainId, updatePriorityFeePerGas]); const substMinerTip = useCallback(() => { - updatePriorityFeePerGas(-calculateMinerTipSubstDifference(maxPriorityFee, txNetwork)); - }, [maxPriorityFee, txNetwork, updatePriorityFeePerGas]); + updatePriorityFeePerGas(-calculateMinerTipSubstDifference(maxPriorityFee, chainId)); + }, [maxPriorityFee, chainId, updatePriorityFeePerGas]); const addMaxFee = useCallback(() => { updateFeePerGas(isL2 ? GAS_FEE_L2_INCREMENT : GAS_FEE_INCREMENT); diff --git a/src/components/expanded-state/swap-details/CurrencyTile.js b/src/components/expanded-state/swap-details/CurrencyTile.js index dee048e8fe9..62072cc8930 100644 --- a/src/components/expanded-state/swap-details/CurrencyTile.js +++ b/src/components/expanded-state/swap-details/CurrencyTile.js @@ -78,7 +78,7 @@ export default function CurrencyTile({ { const blockExplorerText = lang.t('expanded_state.swap.view_on', { - blockExplorerName: startCase(ethereumUtils.getBlockExplorer(chainId)), + blockExplorerName: startCase(ethereumUtils.getBlockExplorer({ chainId })), }); return { actionKey: ContractActionsEnum.blockExplorer, @@ -105,7 +105,7 @@ export default function SwapDetailsContractRow({ asset, onCopySwapDetailsText, . const [menuVisible, setMenuVisible] = useState(false); const menuConfig = useMemo(() => { - const blockExplorerAction = buildBlockExplorerAction(ethereumUtils.getChainIdFromNetwork(asset?.network)); + const blockExplorerAction = buildBlockExplorerAction(asset?.chainId); return { menuItems: [ blockExplorerAction, @@ -116,14 +116,14 @@ export default function SwapDetailsContractRow({ asset, onCopySwapDetailsText, . ], menuTitle: `${asset?.name} (${asset?.symbol})`, }; - }, [asset?.address, asset?.name, asset?.symbol, asset?.network]); + }, [asset?.chainId, asset?.address, asset?.name, asset?.symbol]); const handlePressMenuItem = useCallback( ({ nativeEvent: { actionKey } }) => { if (actionKey === ContractActionsEnum.copyAddress) { handleCopyContractAddress(asset?.address); } else if (actionKey === ContractActionsEnum.blockExplorer) { - ethereumUtils.openTokenEtherscanURL(asset?.address, asset?.network); + ethereumUtils.openTokenEtherscanURL(asset?.address, asset?.chainId); } }, [asset, handleCopyContractAddress] @@ -131,7 +131,7 @@ export default function SwapDetailsContractRow({ asset, onCopySwapDetailsText, . const onPressAndroid = useCallback(() => { const blockExplorerText = lang.t('expanded_state.swap.view_on', { - blockExplorerName: startCase(ethereumUtils.getBlockExplorer(ethereumUtils.getChainIdFromNetwork(asset?.network))), + blockExplorerName: startCase(ethereumUtils.getBlockExplorer({ chainId: asset?.chainId })), }); const androidContractActions = [lang.t('wallet.action.copy_contract_address'), blockExplorerText, lang.t('button.cancel')]; showActionSheetWithOptions( @@ -146,7 +146,7 @@ export default function SwapDetailsContractRow({ asset, onCopySwapDetailsText, . handleCopyContractAddress(asset?.address); } if (idx === 1) { - ethereumUtils.openTokenEtherscanURL(asset?.address, asset?.network); + ethereumUtils.openTokenEtherscanURL(asset?.address, asset?.chainId); } } ); diff --git a/src/components/expanded-state/swap-details/SwapDetailsExchangeRow.js b/src/components/expanded-state/swap-details/SwapDetailsExchangeRow.js index fb9a5852652..67efaa8db35 100644 --- a/src/components/expanded-state/swap-details/SwapDetailsExchangeRow.js +++ b/src/components/expanded-state/swap-details/SwapDetailsExchangeRow.js @@ -1,7 +1,6 @@ import lang from 'i18n-js'; import { capitalize } from 'lodash'; import React, { Fragment, useMemo } from 'react'; -import { CROSSCHAIN_SWAPS, useExperimentalFlag } from '@/config'; import { convertAmountToPercentageDisplay } from '../../../helpers/utilities'; import Pill from '../../Pill'; import { ButtonPressAnimation } from '../../animations'; @@ -11,10 +10,10 @@ import { usePrevious, useStepper } from '@/hooks'; import { ImgixImage } from '@/components/images'; import { getExchangeIconUrl, magicMemo } from '@/utils'; import { SocketBridges } from '@/references/swap/bridges'; -import { RainbowNetworks } from '@/networks'; +import { RainbowNetworkObjects } from '@/networks'; const parseExchangeName = name => { - const networks = RainbowNetworks.map(network => network.name.toLowerCase()); + const networks = RainbowNetworkObjects.map(network => network.name.toLowerCase()); const removeNetworks = name => networks.some(network => name.toLowerCase().includes(network)) ? name.slice(name.indexOf('_') + 1, name.length) : name; diff --git a/src/components/expanded-state/swap-details/SwapDetailsRewardRow.tsx b/src/components/expanded-state/swap-details/SwapDetailsRewardRow.tsx index 657c529d1a5..2dd81818208 100644 --- a/src/components/expanded-state/swap-details/SwapDetailsRewardRow.tsx +++ b/src/components/expanded-state/swap-details/SwapDetailsRewardRow.tsx @@ -9,7 +9,7 @@ import { ChainBadge } from '@/components/coin-icon'; import { getNetworkObject } from '@/networks'; import { useTheme } from '@/theme'; import * as i18n from '@/languages'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; export function SwapDetailsRewardRow({ reward }: { reward: Reward }) { const { navigate } = useNavigation(); diff --git a/src/components/expanded-state/swap-settings/MaxToleranceInput.tsx b/src/components/expanded-state/swap-settings/MaxToleranceInput.tsx index 98f5ceeb63d..d2413fed5f1 100644 --- a/src/components/expanded-state/swap-settings/MaxToleranceInput.tsx +++ b/src/components/expanded-state/swap-settings/MaxToleranceInput.tsx @@ -6,13 +6,13 @@ import { ButtonPressAnimation } from '../../animations'; import { Icon } from '../../icons'; import StepButtonInput from './StepButtonInput'; import { AccentColorProvider, Box, Column, Columns, Inline, Stack, Text } from '@/design-system'; -import { Network } from '@/helpers'; import { add, convertNumberToString, greaterThan } from '@/helpers/utilities'; import { useMagicAutofocus, useSwapSettings } from '@/hooks'; import { useNavigation } from '@/navigation'; import Routes from '@/navigation/routesNames'; import { colors } from '@/styles'; import { ethereumUtils } from '@/utils'; +import { ChainId } from '@/networks/types'; const convertBipsToPercent = (bips: number) => (bips / 100).toString(); const convertPercentToBips = (percent: number) => (percent * 100).toString(); @@ -20,133 +20,131 @@ const convertPercentToBips = (percent: number) => (percent * 100).toString(); const SLIPPAGE_INCREMENT = 0.1; // eslint-disable-next-line react/display-name -export const MaxToleranceInput = forwardRef( - ({ colorForAsset, currentNetwork }: { colorForAsset: string; currentNetwork: Network }, ref) => { - const { slippageInBips, updateSwapSlippage } = useSwapSettings(); - const { navigate } = useNavigation(); - - const [slippageValue, setSlippageValue] = useState(convertBipsToPercent(slippageInBips)); - - const slippageRef = useRef(null); - - const { handleFocus } = useMagicAutofocus(slippageRef, undefined, true); - - const { hasPriceImpact, priceImpactColor } = useMemo(() => { - const hasPriceImpact = Number(slippageValue) >= 3; - const priceImpactColor = hasPriceImpact ? colors.orange : null; - return { hasPriceImpact, priceImpactColor }; - }, [slippageValue]); - - useImperativeHandle(ref, () => ({ - blur: () => { - slippageRef?.current?.blur(); - }, - reset: () => { - const slippage = getDefaultSlippageFromConfig(ethereumUtils.getChainIdFromNetwork(currentNetwork)) as unknown as number; - onSlippageChange(convertBipsToPercent(slippage)); - }, - })); - - const updateSlippage = useCallback( - (increment: any) => { - const newSlippage = add(slippageValue, increment); - const newSlippageValue = convertNumberToString(newSlippage); - if (greaterThan(0, newSlippageValue)) return; - - // @ts-expect-error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'. - updateSwapSlippage(convertPercentToBips(parseFloat(newSlippageValue))); - setSlippageValue(newSlippageValue); - }, - [slippageValue, updateSwapSlippage] - ); - - const addSlippage = useCallback(() => { - updateSlippage(SLIPPAGE_INCREMENT); - }, [updateSlippage]); - - const minusSlippage = useCallback(() => { - updateSlippage(-SLIPPAGE_INCREMENT); - }, [updateSlippage]); - - const onSlippageChange = useCallback( - (value: any) => { - // @ts-expect-error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'. - updateSwapSlippage(convertPercentToBips(value)); - setSlippageValue(value); - }, - [updateSwapSlippage, setSlippageValue] - ); - - const openSlippageExplainer = () => { - Keyboard.dismiss(); - navigate(Routes.EXPLAIN_SHEET, { - type: 'slippage', - }); - }; - - return ( - - - - - - - {`${lang.t('exchange.slippage_tolerance')} `} - {!hasPriceImpact && ( - - {' 􀅵'} - - )} - - {hasPriceImpact && ( - - - +export const MaxToleranceInput = forwardRef(({ colorForAsset, chainId }: { colorForAsset: string; chainId: ChainId }, ref) => { + const { slippageInBips, updateSwapSlippage } = useSwapSettings(); + const { navigate } = useNavigation(); + + const [slippageValue, setSlippageValue] = useState(convertBipsToPercent(slippageInBips)); + + const slippageRef = useRef(null); + + const { handleFocus } = useMagicAutofocus(slippageRef, undefined, true); + + const { hasPriceImpact, priceImpactColor } = useMemo(() => { + const hasPriceImpact = Number(slippageValue) >= 3; + const priceImpactColor = hasPriceImpact ? colors.orange : null; + return { hasPriceImpact, priceImpactColor }; + }, [slippageValue]); + + useImperativeHandle(ref, () => ({ + blur: () => { + slippageRef?.current?.blur(); + }, + reset: () => { + const slippage = getDefaultSlippageFromConfig(chainId) as unknown as number; + onSlippageChange(convertBipsToPercent(slippage)); + }, + })); + + const updateSlippage = useCallback( + (increment: any) => { + const newSlippage = add(slippageValue, increment); + const newSlippageValue = convertNumberToString(newSlippage); + if (greaterThan(0, newSlippageValue)) return; + + // @ts-expect-error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'. + updateSwapSlippage(convertPercentToBips(parseFloat(newSlippageValue))); + setSlippageValue(newSlippageValue); + }, + [slippageValue, updateSwapSlippage] + ); + + const addSlippage = useCallback(() => { + updateSlippage(SLIPPAGE_INCREMENT); + }, [updateSlippage]); + + const minusSlippage = useCallback(() => { + updateSlippage(-SLIPPAGE_INCREMENT); + }, [updateSlippage]); + + const onSlippageChange = useCallback( + (value: any) => { + // @ts-expect-error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'. + updateSwapSlippage(convertPercentToBips(value)); + setSlippageValue(value); + }, + [updateSwapSlippage, setSlippageValue] + ); + + const openSlippageExplainer = () => { + Keyboard.dismiss(); + navigate(Routes.EXPLAIN_SHEET, { + type: 'slippage', + }); + }; + + return ( + + + + + + + {`${lang.t('exchange.slippage_tolerance')} `} + {!hasPriceImpact && ( + + {' 􀅵'} + )} - + + {hasPriceImpact && ( + + + + )} + + + {hasPriceImpact && ( + + + + + {lang.t('exchange.high')} + + + {` · ${lang.t('exchange.price_impact.label')}`} + - {hasPriceImpact && ( - - - - - {lang.t('exchange.high')} - - - {` · ${lang.t('exchange.price_impact.label')}`} - - - )} - - - - - - - ); - } -); + )} + + + + + + + ); +}); diff --git a/src/components/expanded-state/swap-settings/SwapSettingsState.js b/src/components/expanded-state/swap-settings/SwapSettingsState.js index 13d2ae8899e..7432ebece10 100644 --- a/src/components/expanded-state/swap-settings/SwapSettingsState.js +++ b/src/components/expanded-state/swap-settings/SwapSettingsState.js @@ -32,7 +32,7 @@ function useAndroidDisableGesturesOnFocus() { export default function SwapSettingsState({ asset }) { const { flashbotsEnabled, settingsChangeFlashbotsEnabled } = useAccountSettings(); const { - params: { swapSupportsFlashbots = false, network }, + params: { swapSupportsFlashbots = false, chainId }, } = useRoute(); const { colors } = useTheme(); const { setParams, goBack } = useNavigation(); @@ -151,7 +151,7 @@ export default function SwapSettingsState({ asset }) { )} - + diff --git a/src/components/expanded-state/unique-token/NFTBriefTokenInfoRow.tsx b/src/components/expanded-state/unique-token/NFTBriefTokenInfoRow.tsx index 21f6e2cd2f5..8247be503b0 100644 --- a/src/components/expanded-state/unique-token/NFTBriefTokenInfoRow.tsx +++ b/src/components/expanded-state/unique-token/NFTBriefTokenInfoRow.tsx @@ -54,7 +54,7 @@ export default function NFTBriefTokenInfoRow({ asset }: { asset: UniqueAsset }) const { data: listing } = useNFTListing({ contractAddress: asset?.asset_contract?.address ?? '', tokenId: asset?.id, - network: asset?.network, + chainId: asset?.chainId, }); const listingValue = listing && convertRawAmountToRoundedDecimal(listing?.price, listing?.payment_token?.decimals, 3); diff --git a/src/components/expanded-state/unique-token/UniqueTokenExpandedStateHeader.tsx b/src/components/expanded-state/unique-token/UniqueTokenExpandedStateHeader.tsx index 08fb711b91e..205b88c4bff 100644 --- a/src/components/expanded-state/unique-token/UniqueTokenExpandedStateHeader.tsx +++ b/src/components/expanded-state/unique-token/UniqueTokenExpandedStateHeader.tsx @@ -9,7 +9,6 @@ import saveToCameraRoll from './saveToCameraRoll'; import ContextMenuButton from '@/components/native-context-menu/contextMenu'; import { Bleed, Column, Columns, Heading, Inline, Inset, Space, Stack, Text } from '@/design-system'; import { UniqueAsset } from '@/entities'; -import { Network } from '@/helpers'; import { useClipboard, useDimensions, useHiddenTokens, useShowcaseTokens } from '@/hooks'; import { ImgixImage } from '@/components/images'; import { useNavigation } from '@/navigation/Navigation'; @@ -23,6 +22,7 @@ import { refreshNFTContractMetadata, reportNFT } from '@/resources/nfts/simpleha import { ContextCircleButton } from '@/components/context-menu'; import { IS_ANDROID, IS_IOS } from '@/env'; import { MenuActionConfig, MenuConfig } from 'react-native-ios-context-menu'; +import { ChainId } from '@/networks/types'; const AssetActionsEnum = { copyTokenID: 'copyTokenID', @@ -36,7 +36,7 @@ const AssetActionsEnum = { report: 'report', } as const; -const getAssetActions = (network: Network) => +const getAssetActions = ({ chainId }: { chainId: ChainId }) => ({ [AssetActionsEnum.copyTokenID]: { actionKey: AssetActionsEnum.copyTokenID, @@ -57,7 +57,7 @@ const getAssetActions = (network: Network) => [AssetActionsEnum.etherscan]: { actionKey: AssetActionsEnum.etherscan, actionTitle: lang.t('expanded_state.unique_expanded.view_on_block_explorer', { - blockExplorerName: startCase(ethereumUtils.getBlockExplorer(ethereumUtils.getChainIdFromNetwork(network))), + blockExplorerName: startCase(ethereumUtils.getBlockExplorer({ chainId })), }), icon: { iconType: 'SYSTEM', @@ -263,7 +263,7 @@ const UniqueTokenExpandedStateHeader = ({ const isPhotoDownloadAvailable = !isSVG && !isENS; const assetMenuConfig: MenuConfig = useMemo(() => { - const AssetActions = getAssetActions(asset.network); + const AssetActions = getAssetActions({ chainId: asset.chainId }); return { menuItems: [ @@ -309,7 +309,7 @@ const UniqueTokenExpandedStateHeader = ({ { ...AssetActions[AssetActionsEnum.etherscan], }, - ...(asset.network === Network.mainnet + ...(asset.chainId === ChainId.mainnet ? [ { menuTitle: lang.t('expanded_state.unique_expanded.view_on_marketplace'), @@ -320,7 +320,7 @@ const UniqueTokenExpandedStateHeader = ({ ], menuTitle: '', }; - }, [asset.id, asset?.network, isPhotoDownloadAvailable, isHiddenAsset, isModificationActionsEnabled, isSupportedOnRainbowWeb]); + }, [asset.id, asset.chainId, isModificationActionsEnabled, isHiddenAsset, isPhotoDownloadAvailable, isSupportedOnRainbowWeb]); const handlePressFamilyMenuItem = useCallback( // @ts-expect-error ContextMenu is an untyped JS component and can't type its onPress handler properly diff --git a/src/components/gas/GasSpeedButton.js b/src/components/gas/GasSpeedButton.js index 8ba909abe83..a8851d7d387 100644 --- a/src/components/gas/GasSpeedButton.js +++ b/src/components/gas/GasSpeedButton.js @@ -28,7 +28,7 @@ import { getNetworkObject } from '@/networks'; import { IS_ANDROID } from '@/env'; import { ContextMenu } from '../context-menu'; import { EthCoinIcon } from '../coin-icon/EthCoinIcon'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; const { GAS_EMOJIS, GAS_ICONS, GasSpeedOrder, CUSTOM, URGENT, NORMAL, FAST, getGasLabel } = gasUtils; @@ -310,9 +310,9 @@ const GasSpeedButton = ({ type: 'crossChainGas', }); } else { - const nativeAsset = await ethereumUtils.getNativeAssetForNetwork(chainId); + const nativeAsset = await ethereumUtils.getNativeAssetForNetwork({ chainId }); navigate(Routes.EXPLAIN_SHEET, { - network: ethereumUtils.getNetworkFromChainId(chainId), + chainId, type: 'gas', nativeAsset, }); diff --git a/src/components/positions/PositionsCard.tsx b/src/components/positions/PositionsCard.tsx index 366e9c3577d..c1faa16a794 100644 --- a/src/components/positions/PositionsCard.tsx +++ b/src/components/positions/PositionsCard.tsx @@ -18,6 +18,7 @@ import RainbowCoinIcon from '../coin-icon/RainbowCoinIcon'; import { useAccountSettings } from '@/hooks'; import { useExternalToken } from '@/resources/assets/externalAssetsQuery'; import { ethereumUtils } from '@/utils'; +import { AddressOrEth } from '@/__swaps__/types/assets'; type PositionCardProps = { position: RainbowPosition; @@ -33,7 +34,7 @@ function CoinIconForStack({ token }: { token: CoinStackToken }) { const theme = useTheme(); const { nativeCurrency } = useAccountSettings(); const chainId = ethereumUtils.getChainIdFromNetwork(token.network); - const { data: externalAsset } = useExternalToken({ address: token.address, chainId, currency: nativeCurrency }); + const { data: externalAsset } = useExternalToken({ address: token.address as AddressOrEth, chainId, currency: nativeCurrency }); return ( = async ({ assetAddress, network }) => { - const { accountAddress, nativeCurrency } = store.getState().settings; - - const assets = await fetchUserAssets({ - address: accountAddress, - currency: nativeCurrency, - connectedToHardhat: false, - }); - if (!assets || Object.keys(assets).length === 0) return false; - - const desiredAsset = Object.values(assets).find(asset => { - if (!network) { - return asset.uniqueId.toLowerCase() === assetAddress.toLowerCase(); - } - - return asset.uniqueId.toLowerCase() === assetAddress.toLowerCase() && asset.network === network; - }); - if (!desiredAsset) return false; - - return Number(desiredAsset.balance?.amount) > 0; -}; diff --git a/src/components/remote-promo-sheet/check-fns/hasSwapTxn.ts b/src/components/remote-promo-sheet/check-fns/hasSwapTxn.ts index b6133f9f4af..0c7fe5d7fa2 100644 --- a/src/components/remote-promo-sheet/check-fns/hasSwapTxn.ts +++ b/src/components/remote-promo-sheet/check-fns/hasSwapTxn.ts @@ -1,5 +1,5 @@ import type { EthereumAddress, RainbowTransaction } from '@/entities'; -import { RainbowNetworks } from '@/networks'; +import { RainbowNetworkObjects } from '@/networks'; import { queryClient } from '@/react-query/queryClient'; import store from '@/redux/store'; import { consolidatedTransactionsQueryKey } from '@/resources/transactions/consolidatedTransactions'; @@ -13,7 +13,7 @@ const isSwapTx = (tx: RainbowTransaction): boolean => tx.to?.toLowerCase() === R export const hasSwapTxn = async (): Promise => { const { accountAddress, nativeCurrency } = store.getState().settings; - const chainIds = RainbowNetworks.filter(network => network.enabled && network.networkType !== 'testnet').map(network => network.id); + const chainIds = RainbowNetworkObjects.filter(network => network.enabled && network.networkType !== 'testnet').map(network => network.id); const paginatedTransactionsKey = consolidatedTransactionsQueryKey({ address: accountAddress, diff --git a/src/components/remote-promo-sheet/check-fns/index.ts b/src/components/remote-promo-sheet/check-fns/index.ts index 94ec08d36ec..b3fdb065edb 100644 --- a/src/components/remote-promo-sheet/check-fns/index.ts +++ b/src/components/remote-promo-sheet/check-fns/index.ts @@ -1,5 +1,4 @@ export * from './hasNftOffers'; -export * from './hasNonZeroAssetBalance'; export * from './hasNonZeroTotalBalance'; export * from './hasSwapTxn'; export * from './isAfterCampaignLaunch'; diff --git a/src/components/sheet/sheet-action-buttons/SwapActionButton.tsx b/src/components/sheet/sheet-action-buttons/SwapActionButton.tsx index 810bb9e7890..af2a29de48b 100644 --- a/src/components/sheet/sheet-action-buttons/SwapActionButton.tsx +++ b/src/components/sheet/sheet-action-buttons/SwapActionButton.tsx @@ -85,7 +85,7 @@ function SwapActionButton({ asset, color: givenColor, inputType, label, fromDisc if (inputType === assetInputTypes.in) { swapsStore.setState({ inputAsset: userAsset || parsedAsset }); - const nativeAssetForChain = await ethereumUtils.getNativeAssetForNetwork(chainId); + const nativeAssetForChain = await ethereumUtils.getNativeAssetForNetwork({ chainId }); if (nativeAssetForChain && !isSameAsset({ address: nativeAssetForChain.address as AddressOrEth, chainId }, parsedAsset)) { const userOutputAsset = userAssetsStore.getState().getUserAsset(`${nativeAssetForChain.address}_${chainId}`); diff --git a/src/components/toasts/OfflineToast.js b/src/components/toasts/OfflineToast.js index defab4a11ee..ec4565c57e9 100644 --- a/src/components/toasts/OfflineToast.js +++ b/src/components/toasts/OfflineToast.js @@ -1,15 +1,15 @@ import lang from 'i18n-js'; import React from 'react'; import { web3Provider } from '../../handlers/web3'; -import networkTypes from '../../helpers/networkTypes'; import Toast from './Toast'; import { useAccountSettings, useInternetStatus } from '@/hooks'; +import { Network } from '@/networks/types'; const OfflineToast = () => { const isConnected = useInternetStatus(); const { network } = useAccountSettings(); const providerUrl = web3Provider?.connection?.url; - const isMainnet = network === networkTypes.mainnet && !providerUrl?.startsWith('http://'); + const isMainnet = network === Network.mainnet && !providerUrl?.startsWith('http://'); return ; }; diff --git a/src/components/toasts/TestnetToast.js b/src/components/toasts/TestnetToast.js index ec370b3e51b..acbf9afb4ab 100644 --- a/src/components/toasts/TestnetToast.js +++ b/src/components/toasts/TestnetToast.js @@ -1,22 +1,22 @@ import React, { useEffect, useState } from 'react'; -import networkTypes from '../../helpers/networkTypes'; import { Icon } from '../icons'; import { Nbsp, Text } from '../text'; import Toast from './Toast'; -import { isHardHat } from '@/handlers/web3'; import { useInternetStatus } from '@/hooks'; -import { getNetworkObj } from '@/networks'; +import { getNetworkObject } from '@/networks'; +import { ChainId } from '@/networks/types'; +import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; -const TestnetToast = ({ network, web3Provider }) => { +const TestnetToast = ({ chainId }) => { + const { connectedToHardhat } = useConnectedToHardhatStore(); const isConnected = useInternetStatus(); - const providerUrl = web3Provider?.connection?.url; - const { name, colors: networkColors } = getNetworkObj(network); - const [visible, setVisible] = useState(!network === networkTypes.mainnet); + const { name, colors: networkColors } = getNetworkObject({ chainId }); + const [visible, setVisible] = useState(chainId !== ChainId.mainnet); const [networkName, setNetworkName] = useState(name); useEffect(() => { - if (network === networkTypes.mainnet) { - if (isHardHat(providerUrl)) { + if (chainId === ChainId.mainnet) { + if (connectedToHardhat) { setVisible(true); setNetworkName('Hardhat'); } else { @@ -26,7 +26,7 @@ const TestnetToast = ({ network, web3Provider }) => { setVisible(true); setNetworkName(name + (isConnected ? '' : ' (offline)')); } - }, [name, network, providerUrl, isConnected]); + }, [name, isConnected, chainId, connectedToHardhat]); const { colors, isDarkMode } = useTheme(); diff --git a/src/components/token-info/TokenInfoBalanceValue.js b/src/components/token-info/TokenInfoBalanceValue.js index e69519c8811..f9e42c44412 100644 --- a/src/components/token-info/TokenInfoBalanceValue.js +++ b/src/components/token-info/TokenInfoBalanceValue.js @@ -3,7 +3,7 @@ import { RowWithMargins } from '../layout'; import TokenInfoValue from './TokenInfoValue'; import { useColorForAsset } from '@/hooks'; import styled from '@/styled-thing'; -import { ethereumUtils, magicMemo } from '@/utils'; +import { magicMemo } from '@/utils'; import RainbowCoinIcon from '../coin-icon/RainbowCoinIcon'; import { useTheme } from '@/theme'; import { View } from 'react-native'; @@ -28,7 +28,7 @@ const TokenInfoBalanceValue = ({ align, asset, ...props }) => { { - const networkObj = getNetworkObj(ethereumUtils.getNetworkFromChainId(Number(chainId))); + const networkObj = getNetworkObject({ chainId }); return { chainId, color: isDarkMode ? networkObj.colors.dark : networkObj.colors.light, @@ -195,7 +195,7 @@ export default function WalletConnectListItem({ account, chainId, dappIcon, dapp > - + { - const p = await getProviderForNetwork(network); + const chainId = ethereumUtils.getChainIdFromNetwork(network); + const p = await getProvider({ chainId }); const contractInstance = new Contract(TOKEN_GATE_CHECKER_ADDRESS[network], tokenGateCheckerAbi, p); diff --git a/src/graphql/queries/metadata.graphql b/src/graphql/queries/metadata.graphql index e398cbdd2ea..0b38946e07f 100644 --- a/src/graphql/queries/metadata.graphql +++ b/src/graphql/queries/metadata.graphql @@ -571,8 +571,17 @@ query tokenMetadata($address: String!, $chainId: Int!, $currency: String) { } } -query priceChart($chainId: Int!, $address: String!, $day: Boolean!, $hour: Boolean!, $week: Boolean!, $month: Boolean!, $year: Boolean!) { - token(chainID: $chainId, address: $address) { +query priceChart( + $chainId: Int! + $address: String! + $currency: String + $day: Boolean! + $hour: Boolean! + $week: Boolean! + $month: Boolean! + $year: Boolean! +) { + token(chainID: $chainId, address: $address, currency: $currency) { priceCharts { day @include(if: $day) { points diff --git a/src/handlers/__mocks__/web3.ts b/src/handlers/__mocks__/web3.ts index dbd6cbf1714..4b6ff17ecf0 100644 --- a/src/handlers/__mocks__/web3.ts +++ b/src/handlers/__mocks__/web3.ts @@ -1,3 +1,3 @@ import { jest } from '@jest/globals'; -export const getProviderForNetwork = jest.fn(); +export const getProvider = jest.fn(); diff --git a/src/handlers/assets.ts b/src/handlers/assets.ts index 48044fac0a0..2df864afce7 100644 --- a/src/handlers/assets.ts +++ b/src/handlers/assets.ts @@ -2,22 +2,16 @@ import { Contract } from '@ethersproject/contracts'; import { erc20ABI } from '@/references'; import { convertAmountToBalanceDisplay, convertRawAmountToDecimalFormat } from '@/helpers/utilities'; -import { getNetworkObj, getNetworkObject } from '@/networks'; -import { Network } from '@/networks/types'; -import { ChainId } from '@/__swaps__/types/chains'; -import { ethereumUtils } from '@/utils'; - -export function isL2Asset(network: Network) { - return getNetworkObj(network).networkType === 'layer2'; -} +import { getNetworkObject } from '@/networks'; +import { ChainId } from '@/networks/types'; export function isNativeAsset(address: string, chainId: ChainId) { return getNetworkObject({ chainId }).nativeCurrency.address.toLowerCase() === address?.toLowerCase(); } -export async function getOnchainAssetBalance({ address, decimals, symbol }: any, userAddress: any, network: any, provider: any) { +export async function getOnchainAssetBalance({ address, decimals, symbol }: any, userAddress: any, chainId: ChainId, provider: any) { // Check if it's the native chain asset - if (isNativeAsset(address, ethereumUtils.getChainIdFromNetwork(network))) { + if (isNativeAsset(address, chainId)) { return getOnchainNativeAssetBalance({ decimals, symbol }, userAddress, provider); } return getOnchainTokenBalance({ address, decimals, symbol }, userAddress, provider); diff --git a/src/handlers/deeplinks.ts b/src/handlers/deeplinks.ts index c8914e691bd..33316a8c0a0 100644 --- a/src/handlers/deeplinks.ts +++ b/src/handlers/deeplinks.ts @@ -19,6 +19,13 @@ import { FiatProviderName } from '@/entities/f2c'; import { getPoapAndOpenSheetWithQRHash, getPoapAndOpenSheetWithSecretWord } from '@/utils/poaps'; import { queryClient } from '@/react-query'; import { pointsReferralCodeQueryKey } from '@/resources/points'; +import { useMobileWalletProtocolHost } from '@coinbase/mobile-wallet-protocol-host'; +import { InitialRoute } from '@/navigation/initialRoute'; + +interface DeeplinkHandlerProps extends Pick, 'handleRequestUrl' | 'sendFailureToClient'> { + url: string; + initialRoute: InitialRoute; +} /* * You can test these deeplinks with the following command: @@ -26,7 +33,7 @@ import { pointsReferralCodeQueryKey } from '@/resources/points'; * `xcrun simctl openurl booted "https://link.rainbow.me/0x123"` */ -export default async function handleDeeplink(url: string, initialRoute: any = null) { +export default async function handleDeeplink({ url, initialRoute, handleRequestUrl, sendFailureToClient }: DeeplinkHandlerProps) { if (!url) { logger.warn(`[handleDeeplink]: No url provided`); return; @@ -200,6 +207,16 @@ export default async function handleDeeplink(url: string, initialRoute: any = nu break; } + case 'wsegue': { + const response = await handleRequestUrl(url); + if (response.error) { + // Return error to client app if session is expired or invalid + const { errorMessage, decodedRequest } = response.error; + await sendFailureToClient(errorMessage, decodedRequest); + } + break; + } + default: { logger.debug(`[handleDeeplink]: default`, { url }); const addressOrENS = pathname?.split('/profile/')?.[1] ?? pathname?.split('/')?.[1]; diff --git a/src/handlers/ens.ts b/src/handlers/ens.ts index 5277ac98919..aa039d13a05 100644 --- a/src/handlers/ens.ts +++ b/src/handlers/ens.ts @@ -10,9 +10,8 @@ import { prefetchENSCover } from '../hooks/useENSCover'; import { prefetchENSRecords } from '../hooks/useENSRecords'; import { ENSActionParameters, ENSRapActionType } from '@/raps/common'; import { getENSData, getNameFromLabelhash, saveENSData } from './localstorage/ens'; -import { estimateGasWithPadding, getProviderForNetwork, TokenStandard } from './web3'; +import { estimateGasWithPadding, getProvider, TokenStandard } from './web3'; import { ENSRegistrationRecords, Records, UniqueAsset } from '@/entities'; -import { Network } from '@/helpers'; import { ENS_DOMAIN, ENS_RECORDS, ENSRegistrationTransactionType, generateSalt, getENSExecutionDetails, getNameOwner } from '@/helpers/ens'; import { add } from '@/helpers/utilities'; import { ImgixImage } from '@/components/images'; @@ -25,6 +24,7 @@ import { prefetchENSAddress } from '@/resources/ens/ensAddressQuery'; import { MimeType, handleNFTImages } from '@/utils/handleNFTImages'; import store from '@/redux/store'; import { logger, RainbowError } from '@/logger'; +import { ChainId, Network } from '@/networks/types'; const DUMMY_RECORDS = { description: 'description', @@ -51,6 +51,7 @@ const buildEnsToken = ({ }); return { acquisition_date: undefined, + chainId: ChainId.mainnet, animation_url: null, asset_contract: { address: contractAddress, @@ -321,7 +322,7 @@ export const fetchAccountDomains = async (address: string) => { export const fetchImage = async (imageType: 'avatar' | 'header', ensName: string) => { let imageUrl; - const provider = await getProviderForNetwork(); + const provider = await getProvider({ chainId: ChainId.mainnet }); try { const avatarResolver = new AvatarResolver(provider); imageUrl = await avatarResolver.getImage(ensName, { @@ -346,7 +347,7 @@ export const fetchRecords = async (ensName: string, { supportedOnly = true }: { const data = response.domains[0] || {}; const rawRecordKeys = data.resolver?.texts || []; - const provider = await getProviderForNetwork(); + const provider = await getProvider({ chainId: ChainId.mainnet }); const resolver = await provider.getResolver(ensName); const supportedRecords = Object.values(ENS_RECORDS); const recordKeys = (rawRecordKeys as ENS_RECORDS[]).filter(key => (supportedOnly ? supportedRecords.includes(key) : true)); @@ -368,7 +369,7 @@ export const fetchCoinAddresses = async ( const response = await ensClient.getCoinTypesByName({ name: ensName }); const data = response.domains[0] || {}; const supportedRecords = Object.values(ENS_RECORDS); - const provider = await getProviderForNetwork(); + const provider = await getProvider({ chainId: ChainId.mainnet }); const resolver = await provider.getResolver(ensName); const rawCoinTypes: number[] = data.resolver?.coinTypes || []; const rawCoinTypesNames: string[] = rawCoinTypes.map(type => formatsByCoinType[type].name); @@ -401,7 +402,7 @@ export const fetchCoinAddresses = async ( }; export const fetchContenthash = async (ensName: string) => { - const provider = await getProviderForNetwork(); + const provider = await getProvider({ chainId: ChainId.mainnet }); const resolver = await provider.getResolver(ensName); const contenthash = await resolver?.getContentHash(); return contenthash; @@ -448,7 +449,7 @@ export const fetchRegistration = async (ensName: string) => { }; export const fetchPrimary = async (ensName: string) => { - const provider = await getProviderForNetwork(); + const provider = await getProvider({ chainId: ChainId.mainnet }); const address = await provider.resolveName(ensName); return { address, @@ -887,7 +888,7 @@ export const getRapActionTypeForTxType = (txType: ENSRegistrationTransactionType export const fetchReverseRecord = async (address: string) => { try { const checksumAddress = getAddress(address); - const provider = await getProviderForNetwork(); + const provider = await getProvider({ chainId: ChainId.mainnet }); const reverseRecord = await provider.lookupAddress(checksumAddress); return reverseRecord ?? ''; } catch (e) { @@ -897,7 +898,7 @@ export const fetchReverseRecord = async (address: string) => { export const fetchResolver = async (ensName: string) => { try { - const provider = await getProviderForNetwork(); + const provider = await getProvider({ chainId: ChainId.mainnet }); const resolver = await provider.getResolver(ensName); return resolver ?? ({} as Resolver); } catch (e) { diff --git a/src/handlers/gasFees.ts b/src/handlers/gasFees.ts index aeffb8cddac..6d85e0d1b59 100644 --- a/src/handlers/gasFees.ts +++ b/src/handlers/gasFees.ts @@ -1,5 +1,5 @@ -import { Network } from '@/helpers'; import { RainbowFetchClient } from '../rainbow-fetch'; +import { ChainId, chainIdToNameMapping } from '@/networks/types'; const rainbowMeteorologyApi = new RainbowFetchClient({ baseURL: 'https://metadata.p.rainbow.me', @@ -10,4 +10,5 @@ const rainbowMeteorologyApi = new RainbowFetchClient({ timeout: 30000, // 30 secs }); -export const rainbowMeteorologyGetData = (network: Network) => rainbowMeteorologyApi.get(`/meteorology/v1/gas/${network}`, {}); +export const rainbowMeteorologyGetData = (chainId: ChainId) => + rainbowMeteorologyApi.get(`/meteorology/v1/gas/${chainIdToNameMapping[chainId]}`, {}); diff --git a/src/handlers/localstorage/globalSettings.ts b/src/handlers/localstorage/globalSettings.ts index a9e3769bfbf..c54370a7cd1 100644 --- a/src/handlers/localstorage/globalSettings.ts +++ b/src/handlers/localstorage/globalSettings.ts @@ -1,6 +1,6 @@ +import { ChainId } from '@/networks/types'; import { getGlobal, saveGlobal } from './common'; import { NativeCurrencyKeys } from '@/entities'; -import networkTypes from '@/helpers/networkTypes'; import { Language } from '@/languages'; export const IMAGE_METADATA = 'imageMetadata'; @@ -8,7 +8,7 @@ const KEYBOARD_HEIGHT = 'keyboardHeight'; const APP_ICON = 'appIcon'; const LANGUAGE = 'language'; const NATIVE_CURRENCY = 'nativeCurrency'; -const NETWORK = 'network'; +const CHAIN_ID = 'chainId'; const KEYCHAIN_INTEGRITY_STATE = 'keychainIntegrityState'; const AUTH_TIMELOCK = 'authTimelock'; const PIN_AUTH_ATTEMPTS_LEFT = 'pinAuthAttemptsLeft'; @@ -32,9 +32,9 @@ export const getLanguage = () => getGlobal(LANGUAGE, Language.EN_US); export const saveLanguage = (language: any) => saveGlobal(LANGUAGE, language); -export const getNetwork = () => getGlobal(NETWORK, networkTypes.mainnet); +export const getChainId = () => getGlobal(CHAIN_ID, ChainId.mainnet); -export const saveNetwork = (network: any) => saveGlobal(NETWORK, network); +export const saveChainId = (chainId: ChainId) => saveGlobal(CHAIN_ID, chainId); export const getKeyboardHeight = () => getGlobal(KEYBOARD_HEIGHT, null); diff --git a/src/handlers/localstorage/removeWallet.ts b/src/handlers/localstorage/removeWallet.ts index 7364f02e0cc..1a2934127be 100644 --- a/src/handlers/localstorage/removeWallet.ts +++ b/src/handlers/localstorage/removeWallet.ts @@ -1,17 +1,17 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; import { keys } from 'lodash'; -import NetworkTypes from '../../helpers/networkTypes'; import { accountLocalKeys } from './accountLocal'; import { getKey } from './common'; import { walletConnectAccountLocalKeys } from './walletconnectRequests'; import { logger, RainbowError } from '@/logger'; import { removeNotificationSettingsForWallet } from '@/notifications/settings'; +import { Network } from '@/networks/types'; export const removeWalletData = async (accountAddress: any) => { logger.debug('[localstorage/removeWallet]: removing wallet data', { accountAddress }); const allPrefixes = accountLocalKeys.concat(walletConnectAccountLocalKeys); logger.debug('[localstorage/removeWallet]: all prefixes', { allPrefixes }); - const networks = keys(NetworkTypes); + const networks = keys(Network); const allKeysWithNetworks = allPrefixes.map(prefix => networks.map(network => getKey(prefix, accountAddress, network))); const allKeys = allKeysWithNetworks.flat(); try { diff --git a/src/handlers/localstorage/theme.ts b/src/handlers/localstorage/theme.ts index 32011a99809..a18f805da04 100644 --- a/src/handlers/localstorage/theme.ts +++ b/src/handlers/localstorage/theme.ts @@ -2,7 +2,7 @@ import { IS_ANDROID } from '@/env'; import { getGlobal, saveGlobal } from './common'; import { NativeModules } from 'react-native'; import { colors } from '@/styles'; -import { isUsingButtonNavigation } from '@/helpers/statusBarHelper'; +import { isUsingButtonNavigation } from '@/utils/deviceUtils'; import { Themes, ThemesType } from '@/theme'; const { NavigationBar } = NativeModules; diff --git a/src/handlers/swap.ts b/src/handlers/swap.ts index 51c4b9de402..d68138be447 100644 --- a/src/handlers/swap.ts +++ b/src/handlers/swap.ts @@ -15,14 +15,14 @@ import { Contract } from '@ethersproject/contracts'; import { MaxUint256 } from '@ethersproject/constants'; import { IS_TESTING } from 'react-native-dotenv'; import { Token } from '../entities/tokens'; -import { estimateGasWithPadding, getProviderForNetwork, toHexNoLeadingZeros } from './web3'; +import { estimateGasWithPadding, getProvider, toHexNoLeadingZeros } from './web3'; import { getRemoteConfig } from '@/model/remoteConfig'; import { Asset } from '@/entities'; import { add, convertRawAmountToDecimalFormat, divide, lessThan, multiply, subtract } from '@/helpers/utilities'; import { erc20ABI, ethUnits } from '@/references'; import { ethereumUtils } from '@/utils'; -import { ChainId } from '@/__swaps__/types/chains'; import { logger, RainbowError } from '@/logger'; +import { ChainId } from '@/networks/types'; export enum Field { INPUT = 'INPUT', @@ -234,8 +234,7 @@ export const estimateSwapGasLimit = async ({ requiresApprove?: boolean; tradeDetails: Quote | null; }): Promise => { - const network = ethereumUtils.getNetworkFromChainId(chainId); - const provider = await getProviderForNetwork(network); + const provider = await getProvider({ chainId }); if (!provider || !tradeDetails) { return ethereumUtils.getBasicSwapGasLimit(Number(chainId)); } @@ -311,8 +310,7 @@ export const estimateCrosschainSwapGasLimit = async ({ requiresApprove?: boolean; tradeDetails: CrosschainQuote; }): Promise => { - const network = ethereumUtils.getNetworkFromChainId(chainId); - const provider = await getProviderForNetwork(network); + const provider = await getProvider({ chainId }); if (!provider || !tradeDetails) { return ethereumUtils.getBasicSwapGasLimit(Number(chainId)); } diff --git a/src/handlers/tokenSearch.ts b/src/handlers/tokenSearch.ts index 848fc0cd770..00a54669b06 100644 --- a/src/handlers/tokenSearch.ts +++ b/src/handlers/tokenSearch.ts @@ -93,12 +93,13 @@ export const tokenSearch = async (searchParams: { return tokenSearch.data.data.map(token => { const networkKeys = Object.keys(token.networks); - const network = ethereumUtils.getNetworkFromChainId(Number(networkKeys[0])); + const chainId = Number(networkKeys[0]); + const network = ethereumUtils.getNetworkFromChainId(chainId); return { ...token, - address: token.networks['1']?.address || token.networks[Number(networkKeys[0])]?.address, + chainId, + address: token.networks['1']?.address || token.networks[chainId]?.address, network, - chainId: searchParams.chainId, mainnet_address: token.networks['1']?.address, }; }); diff --git a/src/handlers/web3.ts b/src/handlers/web3.ts index 2d793470994..015e75eea6d 100644 --- a/src/handlers/web3.ts +++ b/src/handlers/web3.ts @@ -10,7 +10,6 @@ import { startsWith } from 'lodash'; import { getRemoteConfig } from '@/model/remoteConfig'; import { AssetType, NewTransaction, ParsedAddressAsset } from '@/entities'; import { isNativeAsset } from '@/handlers/assets'; -import { Network } from '@/helpers/networkTypes'; import { isUnstoppableAddressFormat } from '@/helpers/validators'; import { ARBITRUM_ETH_ADDRESS, @@ -36,17 +35,16 @@ import { import { ethereumUtils } from '@/utils'; import { logger, RainbowError } from '@/logger'; import { IS_IOS, RPC_PROXY_API_KEY, RPC_PROXY_BASE_URL } from '@/env'; -import { getNetworkObj, getNetworkObject } from '@/networks'; +import { getNetworkObject } from '@/networks'; import store from '@/redux/store'; -import { getNetworkFromChainId } from '@/utils/ethereumUtils'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; export enum TokenStandard { ERC1155 = 'ERC1155', ERC721 = 'ERC721', } -export const networkProviders = new Map(); +export const chainsProviders = new Map(); /** * Creates an rpc endpoint for a given chain id using the Rainbow rpc proxy. @@ -73,30 +71,29 @@ export const proxyRpcEndpoint = (chainId: number, customEndpoint?: string) => { }`; } else { if (customEndpoint) return customEndpoint; - const network = ethereumUtils.getNetworkFromChainId(chainId); - switch (network) { - case Network.arbitrum: + switch (chainId) { + case ChainId.arbitrum: return arbitrum_mainnet_rpc; - case Network.goerli: + case ChainId.goerli: return ethereum_goerli_rpc; - case Network.optimism: + case ChainId.optimism: return optimism_mainnet_rpc; - case Network.polygon: + case ChainId.polygon: return polygon_mainnet_rpc; - case Network.base: + case ChainId.base: return base_mainnet_rpc; - case Network.bsc: + case ChainId.bsc: return bsc_mainnet_rpc; - case Network.zora: + case ChainId.zora: return zora_mainnet_rpc; - case Network.avalanche: + case ChainId.avalanche: return avalanche_mainnet_rpc; - case Network.blast: + case ChainId.blast: return blast_mainnet_rpc; - case Network.degen: + case ChainId.degen: return degen_mainnet_rpc; - case Network.gnosis: - case Network.mainnet: + case ChainId.gnosis: + case ChainId.mainnet: default: return ethereum_mainnet_rpc; } @@ -119,7 +116,7 @@ type GasParamsInput = { gasPrice: BigNumberish } & { /** * The input data provied to `getTxDetails`. */ -type TransactionDetailsInput = Pick & +type TransactionDetailsInput = Pick & Pick & GasParamsInput; @@ -130,7 +127,7 @@ type TransactionDetailsReturned = { data?: TransactionRequest['data']; from?: TransactionRequest['from']; gasLimit?: string; - network?: Network | string; + chainId?: ChainId | string; to?: TransactionRequest['to']; value?: TransactionRequest['value']; nonce?: TransactionRequest['nonce']; @@ -150,23 +147,13 @@ type NewTransactionNonNullable = { */ export let web3Provider: StaticJsonRpcProvider = null as unknown as StaticJsonRpcProvider; -/** - * @desc Checks whether or not a `Network | string` union type should be - * treated as a `Network` based on its prefix, as opposed to a `string` type. - * @param network The network to check. - * @return A type predicate of `network is Network`. - */ -const isNetworkEnum = (network: Network | string): network is Network => { - return !network.startsWith('http://'); -}; - /** * @desc Sets a different web3 provider. * @param network The network to set. * @return A promise that resolves with an Ethers Network when the provider is ready. */ -export const web3SetHttpProvider = async (network: Network | string): Promise => { - web3Provider = await getProviderForNetwork(network); +export const web3SetHttpProvider = async (chainId: ChainId): Promise => { + web3Provider = await getProvider({ chainId }); return web3Provider.ready; }; @@ -179,92 +166,42 @@ export const isL2Chain = ({ chainId }: { chainId: ChainId }): boolean => { return getNetworkObject({ chainId }).networkType === 'layer2'; }; -/** - * @desc Checks whether a provider is HardHat. - * @param providerUrl The provider URL. - * @return Whether or not the provider is HardHat. - */ -export const isHardHat = (providerUrl: string): boolean => { - return providerUrl?.startsWith('http://') && providerUrl?.endsWith('8545'); -}; - /** * @desc Checks if the given network is a testnet. * @param network The network to check. * @return Whether or not the network is a testnet. */ -export const isTestnetNetwork = (network: Network): boolean => { - return getNetworkObj(network as Network).networkType === 'testnet'; +export const isTestnetChain = ({ chainId }: { chainId: ChainId }): boolean => { + return getNetworkObject({ chainId }).networkType === 'testnet'; }; // shoudl figure out better way to include this in networks export const getFlashbotsProvider = async () => { return new StaticJsonRpcProvider( proxyRpcEndpoint( - 1, + ChainId.mainnet, 'https://rpc.flashbots.net/?hint=hash&builder=flashbots&builder=f1b.io&builder=rsync&builder=beaverbuild.org&builder=builder0x69&builder=titan&builder=eigenphi&builder=boba-builder' - ), - Network.mainnet + ) ); }; -export const getCachedProviderForNetwork = (network: Network = Network.mainnet): StaticJsonRpcProvider | undefined => { - return networkProviders.get(network); -}; - -/** - * @desc Gets or constructs a web3 provider for the specified network. - * @param network The network as a `Network` or string. - * @return The provider for the network. - */ -export const getProviderForNetwork = (network: Network | string = Network.mainnet): StaticJsonRpcProvider => { - const isSupportedNetwork = isNetworkEnum(network); - const cachedProvider = isSupportedNetwork ? networkProviders.get(network) : undefined; - - if (isSupportedNetwork && cachedProvider) { - return cachedProvider; - } - - if (!isSupportedNetwork) { - const provider = new StaticJsonRpcProvider(network, Network.mainnet); - networkProviders.set(Network.mainnet, provider); - return provider; - } else { - const provider = new StaticJsonRpcProvider(getNetworkObj(network).rpc(), getNetworkObj(network).id); - networkProviders.set(network, provider); - return provider; - } +export const getCachedProviderForNetwork = (chainId: ChainId = ChainId.mainnet): StaticJsonRpcProvider | undefined => { + return chainsProviders.get(chainId); }; export const getProvider = ({ chainId }: { chainId: number }): StaticJsonRpcProvider => { - const network = getNetworkFromChainId(chainId); - const isSupportedNetwork = isNetworkEnum(network); - const cachedProvider = isSupportedNetwork ? networkProviders.get(network) : undefined; + const cachedProvider = chainsProviders.get(chainId); - if (isSupportedNetwork && cachedProvider) { + const networkObject = getNetworkObject({ chainId }); + + if (cachedProvider && cachedProvider?.connection.url === networkObject.rpc()) { return cachedProvider; } - if (!isSupportedNetwork) { - const provider = new StaticJsonRpcProvider(network, Network.mainnet); - networkProviders.set(Network.mainnet, provider); - return provider; - } else { - const provider = new StaticJsonRpcProvider(getNetworkObj(network).rpc(), getNetworkObj(network).id); - networkProviders.set(network, provider); - return provider; - } -}; + const provider = new StaticJsonRpcProvider(networkObject.rpc(), networkObject.id); + chainsProviders.set(chainId, provider); -/** - * @desc Checks if the active network is Hardhat. - * @returns boolean: `true` if connected to Hardhat. - */ -export const getIsHardhatConnected = (): boolean => { - const currentNetwork = store.getState().settings.network; - const currentProviderUrl = getCachedProviderForNetwork(currentNetwork)?.connection?.url; - const connectedToHardhat = !!currentProviderUrl && isHardHat(currentProviderUrl); - return connectedToHardhat; + return provider; }; /** @@ -500,8 +437,8 @@ export const getTransactionCount = async (address: string): Promise & GasParamsInput): GasParamsReturned => { - return getNetworkObj(transaction.network).gas.gasType === 'legacy' +export const getTransactionGasParams = (transaction: Pick & GasParamsInput): GasParamsReturned => { + return getNetworkObject({ chainId: transaction.chainId }).gas.gasType === 'legacy' ? { gasPrice: toHex(transaction.gasPrice), } @@ -577,7 +514,7 @@ export const resolveNameOrAddress = async (nameOrAddress: string): Promise ): Promise => { const recipient = await resolveNameOrAddress(transaction.to); @@ -612,7 +549,7 @@ export const getTransferNftTransaction = async ( data, from, gasLimit: transaction.gasLimit?.toString(), - network: transaction.network, + chainId: transaction.chainId, nonce, to: contractAddress, ...gasParams, @@ -628,7 +565,7 @@ export const getTransferNftTransaction = async ( export const getTransferTokenTransaction = async ( transaction: Pick< NewTransactionNonNullable, - 'asset' | 'from' | 'to' | 'amount' | 'gasPrice' | 'gasLimit' | 'network' | 'maxFeePerGas' | 'maxPriorityFeePerGas' + 'asset' | 'from' | 'to' | 'amount' | 'gasPrice' | 'gasLimit' | 'chainId' | 'maxFeePerGas' | 'maxPriorityFeePerGas' > ): Promise => { const value = convertAmountToRawAmount(transaction.amount, transaction.asset.decimals); @@ -639,7 +576,7 @@ export const getTransferTokenTransaction = async ( data, from: transaction.from, gasLimit: transaction.gasLimit?.toString(), - network: transaction.network, + chainId: transaction.chainId, to: transaction.asset.address, ...gasParams, }; @@ -712,10 +649,10 @@ export const getDataForNftTransfer = (from: string, to: string, asset: ParsedAdd const lowercasedContractAddress = asset.asset_contract.address.toLowerCase(); const standard = asset.asset_contract?.schema_name; let data: string | undefined; - if (lowercasedContractAddress === CRYPTO_KITTIES_NFT_ADDRESS && asset.network === Network.mainnet) { + if (lowercasedContractAddress === CRYPTO_KITTIES_NFT_ADDRESS && asset.chainId === ChainId.mainnet) { const transferMethod = smartContractMethods.token_transfer; data = ethereumUtils.getDataString(transferMethod.hash, [ethereumUtils.removeHexPrefix(to), convertStringToHex(asset.id)]); - } else if (lowercasedContractAddress === CRYPTO_PUNKS_NFT_ADDRESS && asset.network === Network.mainnet) { + } else if (lowercasedContractAddress === CRYPTO_PUNKS_NFT_ADDRESS && asset.chainId === ChainId.mainnet) { const transferMethod = smartContractMethods.punk_transfer; data = ethereumUtils.getDataString(transferMethod.hash, [ethereumUtils.removeHexPrefix(to), convertStringToHex(asset.id)]); } else if (standard === TokenStandard.ERC1155) { @@ -744,7 +681,7 @@ export const getDataForNftTransfer = (from: string, to: string, asset: ParsedAdd * @param [{address, amount, asset, gasLimit, recipient}] The transaction * initialization details. * @param provider The RCP provider to use. - * @param network The network for the transaction + * @param chainId The chainId for the transaction * @return The transaction request. */ export const buildTransaction = async ( @@ -762,7 +699,7 @@ export const buildTransaction = async ( gasLimit?: string; }, provider: StaticJsonRpcProvider | null, - network: Network + chainId: ChainId ): Promise => { const _amount = amount && Number(amount) ? convertAmountToRawAmount(amount, asset.decimals) : estimateAssetBalancePortion(asset); const value = _amount.toString(); @@ -781,7 +718,7 @@ export const buildTransaction = async ( from: address, to: contractAddress, }; - } else if (!isNativeAsset(asset.address, ethereumUtils.getChainIdFromNetwork(network))) { + } else if (!isNativeAsset(asset.address, chainId)) { const transferData = getDataForTokenTransfer(value, _recipient); txData = { data: transferData, @@ -801,7 +738,7 @@ export const buildTransaction = async ( * to `false`. * @param provider If provided, a provider to use instead of the default * cached `web3Provider`. - * @param network The network to use, defaulting to `Network.mainnet`. + * @param chainId The chainId to use, defaulting to `ChainId.mainnet`. * @returns The estimated gas limit. */ export const estimateGasLimit = async ( @@ -818,9 +755,9 @@ export const estimateGasLimit = async ( }, addPadding = false, provider: StaticJsonRpcProvider | null = null, - network: Network = Network.mainnet + chainId: ChainId = ChainId.mainnet ): Promise => { - const estimateGasData = await buildTransaction({ address, amount, asset, recipient }, provider, network); + const estimateGasData = await buildTransaction({ address, amount, asset, recipient }, provider, chainId); if (addPadding) { return estimateGasWithPadding(estimateGasData, null, null, provider); diff --git a/src/helpers/RainbowContext.tsx b/src/helpers/RainbowContext.tsx index f38c5ad79c2..c0734b50313 100644 --- a/src/helpers/RainbowContext.tsx +++ b/src/helpers/RainbowContext.tsx @@ -10,12 +10,11 @@ import { useDispatch } from 'react-redux'; import { useTheme } from '../theme/ThemeContext'; import { STORAGE_IDS } from '@/model/mmkv'; import { IS_TESTING } from 'react-native-dotenv'; -import { web3SetHttpProvider } from '@/handlers/web3'; import { logger, RainbowError } from '@/logger'; -import networkTypes from '@/helpers/networkTypes'; import { explorerInit } from '@/redux/explorer'; import { Navigation } from '@/navigation'; import Routes from '@rainbow-me/routes'; +import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; export const RainbowContext = createContext({}); const storageKey = 'config'; @@ -28,6 +27,7 @@ export default function RainbowContextWrapper({ children }: PropsWithChildren) { // This value is hold here to prevent JS VM from shutting down // on unmounting all shared values. useSharedValue(0); + const { setConnectedToHardhat } = useConnectedToHardhatStore(); const [config, setConfig] = useState>( Object.entries(defaultConfig).reduce((acc, [key, { value }]) => ({ ...acc, [key]: value }), {}) ); @@ -63,17 +63,17 @@ export default function RainbowContextWrapper({ children }: PropsWithChildren) { const connectToHardhat = useCallback(async () => { try { - const ready = await web3SetHttpProvider('http://127.0.0.1:8545'); - logger.debug('[RainbowContext]: connected to hardhat', { ready }); + setConnectedToHardhat(true); + logger.debug('connected to hardhat'); } catch (e: any) { - await web3SetHttpProvider(networkTypes.mainnet); - logger.error(new RainbowError('[RainbowContext]: error connecting to hardhat'), { + setConnectedToHardhat(false); + logger.error(new RainbowError('error connecting to hardhat'), { message: e.message, }); } dispatch(explorerInit()); Navigation.handleAction(Routes.WALLET_SCREEN, {}); - }, [dispatch]); + }, [dispatch, setConnectedToHardhat]); return ( diff --git a/src/helpers/accountInfo.ts b/src/helpers/accountInfo.ts index d694523cb71..be67dab7d58 100644 --- a/src/helpers/accountInfo.ts +++ b/src/helpers/accountInfo.ts @@ -1,3 +1,4 @@ +import { RainbowAccount } from '@/model/wallet'; import { removeFirstEmojiFromString, returnStringFirstEmoji } from '../helpers/emojiHandler'; import { address } from '../utils/abbreviations'; import { addressHashedEmoji, isValidImagePath } from '../utils/profileUtils'; @@ -17,8 +18,10 @@ export function getAccountProfileInfo(selectedWallet: any, walletNames: any, acc const accountENS = walletNames?.[accountAddress]; - const selectedAccount = selectedWallet.addresses?.find((account: any) => account.address === accountAddress); - + const lowerCaseAccountAddress = accountAddress.toLowerCase(); + const selectedAccount = selectedWallet.addresses?.find( + (account: RainbowAccount) => account.address?.toLowerCase() === lowerCaseAccountAddress + ); if (!selectedAccount) { return {}; } diff --git a/src/helpers/dappNameHandler.ts b/src/helpers/dappNameHandler.ts index b679aebd122..a3c06dd4c66 100644 --- a/src/helpers/dappNameHandler.ts +++ b/src/helpers/dappNameHandler.ts @@ -5,6 +5,9 @@ export const getDappHostname = memoFn(url => { // @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call. const urlObject = new URL(url); let hostname; + if (!urlObject.hostname) { + return null; + } const subdomains = urlObject.hostname.split('.'); if (subdomains.length === 2) { hostname = urlObject.hostname; diff --git a/src/helpers/ens.ts b/src/helpers/ens.ts index 8a1e81f8e7d..f6be6fdf771 100644 --- a/src/helpers/ens.ts +++ b/src/helpers/ens.ts @@ -8,7 +8,7 @@ import { atom } from 'recoil'; import { InlineFieldProps } from '../components/inputs/InlineField'; import { add, addBuffer, convertAmountAndPriceToNativeDisplay, divide, fromWei, handleSignificantDecimals, multiply } from './utilities'; import { ENSRegistrationRecords, EthereumAddress } from '@/entities'; -import { getProviderForNetwork, toHex } from '@/handlers/web3'; +import { getProvider, toHex } from '@/handlers/web3'; import { gweiToWei } from '@/parsers'; import { ENSBaseRegistrarImplementationABI, @@ -25,6 +25,7 @@ import { import { colors } from '@/styles'; import { labelhash } from '@/utils'; import { encodeContenthash, isValidContenthash } from '@/utils/contenthash'; +import { ChainId } from '@/networks/types'; export const ENS_SECONDS_WAIT = 60; export const ENS_SECONDS_PADDING = 5; @@ -367,27 +368,27 @@ export const deprecatedTextRecordFields = { export const ENS_DOMAIN = '.eth'; const getENSRegistrarControllerContract = async (wallet?: Signer, registrarAddress?: string) => { - const signerOrProvider = wallet || (await getProviderForNetwork()); + const signerOrProvider = wallet || (await getProvider({ chainId: ChainId.mainnet })); return new Contract(registrarAddress || ensETHRegistrarControllerAddress, ENSETHRegistrarControllerABI, signerOrProvider); }; const getENSPublicResolverContract = async (wallet?: Signer, resolverAddress?: EthereumAddress) => { - const signerOrProvider = wallet || (await getProviderForNetwork()); + const signerOrProvider = wallet || (await getProvider({ chainId: ChainId.mainnet })); return new Contract(resolverAddress || ensPublicResolverAddress, ENSPublicResolverABI, signerOrProvider); }; const getENSReverseRegistrarContract = async (wallet?: Signer) => { - const signerOrProvider = wallet || (await getProviderForNetwork()); + const signerOrProvider = wallet || (await getProvider({ chainId: ChainId.mainnet })); return new Contract(ensReverseRegistrarAddress, ENSReverseRegistrarABI, signerOrProvider); }; const getENSBaseRegistrarImplementationContract = async (wallet?: Signer) => { - const signerOrProvider = wallet || (await getProviderForNetwork()); + const signerOrProvider = wallet || (await getProvider({ chainId: ChainId.mainnet })); return new Contract(ensBaseRegistrarImplementationAddress, ENSBaseRegistrarImplementationABI, signerOrProvider); }; const getENSRegistryContract = async (wallet?: Signer) => { - const signerOrProvider = wallet ?? (await getProviderForNetwork()); + const signerOrProvider = wallet ?? (await getProvider({ chainId: ChainId.mainnet })); return new Contract(ensRegistryAddress, ENSRegistryWithFallbackABI, signerOrProvider); }; diff --git a/src/helpers/findWalletWithAccount.ts b/src/helpers/findWalletWithAccount.ts index 109371d2b82..21de1be494e 100644 --- a/src/helpers/findWalletWithAccount.ts +++ b/src/helpers/findWalletWithAccount.ts @@ -1,11 +1,12 @@ -import { RainbowWallet } from '@/model/wallet'; +import { RainbowAccount, RainbowWallet } from '@/model/wallet'; export function findWalletWithAccount(wallets: { [key: string]: RainbowWallet }, accountAddress: string): RainbowWallet | undefined { const sortedKeys = Object.keys(wallets).sort(); let walletWithAccount: RainbowWallet | undefined; + const lowerCaseAccountAddress = accountAddress.toLowerCase(); sortedKeys.forEach(key => { const wallet = wallets[key]; - const found = wallet.addresses?.find((account: any) => account.address === accountAddress); + const found = wallet.addresses?.find((account: RainbowAccount) => account.address?.toLowerCase() === lowerCaseAccountAddress); if (found) { walletWithAccount = wallet; } diff --git a/src/helpers/gas.ts b/src/helpers/gas.ts index 540cf9752ad..69b7a786578 100644 --- a/src/helpers/gas.ts +++ b/src/helpers/gas.ts @@ -1,7 +1,7 @@ -import { Network } from '@/networks/types'; import { memoFn } from '../utils/memoFn'; import { gasUtils } from '@/utils'; -import { getNetworkObj } from '@/networks'; +import { getNetworkObject } from '@/networks'; +import { ChainId } from '@/networks/types'; const { GasTrends } = gasUtils; const { FALLING, NO_TREND, RISING, STABLE, SURGING } = GasTrends; @@ -25,8 +25,8 @@ export const getTrendKey = memoFn((trend: number) => { return NO_TREND; }); -export const calculateMinerTipAddDifference = memoFn((maxPriorityFee: string, txNetwork: Network) => { - const networkObject = getNetworkObj(txNetwork); +export const calculateMinerTipAddDifference = memoFn((maxPriorityFee: string, chainId: ChainId) => { + const networkObject = getNetworkObject({ chainId }); const isL2 = networkObject.networkType === 'layer2'; const FEE_INCREMENT = isL2 ? PRIORITY_FEE_L2_INCREMENT : PRIORITY_FEE_INCREMENT; const FEE_THRESHOLD = isL2 ? PRIORITY_FEE_L2_THRESHOLD : PRIORITY_FEE_THRESHOLD; @@ -38,8 +38,8 @@ export const calculateMinerTipAddDifference = memoFn((maxPriorityFee: string, tx } }); -export const calculateMinerTipSubstDifference = memoFn((maxPriorityFee: string, txNetwork: Network) => { - const networkObject = getNetworkObj(txNetwork); +export const calculateMinerTipSubstDifference = memoFn((maxPriorityFee: string, chainId: ChainId) => { + const networkObject = getNetworkObject({ chainId }); const isL2 = networkObject.networkType === 'layer2'; const FEE_INCREMENT = isL2 ? PRIORITY_FEE_L2_INCREMENT : PRIORITY_FEE_INCREMENT; const FEE_THRESHOLD = isL2 ? PRIORITY_FEE_L2_THRESHOLD : PRIORITY_FEE_THRESHOLD; diff --git a/src/helpers/index.ts b/src/helpers/index.ts index eb9b7a803b2..4a38d19659d 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -5,6 +5,5 @@ export { default as CurrencySelectionTypes } from './currencySelectionTypes'; export { default as ExchangeModalTypes } from './exchangeModalTypes'; export { default as isKeyboardOpen } from './isKeyboardOpen'; export { default as isReanimatedAvailable } from './isReanimatedAvailable'; -export { default as NetworkTypes, Network } from './networkTypes'; export { default as TokenSectionTypes } from './tokenSectionTypes'; export { StatusBarHelper }; diff --git a/src/helpers/networkInfo.ts b/src/helpers/networkInfo.ts index 2bda8506ad8..be81a8bad48 100644 --- a/src/helpers/networkInfo.ts +++ b/src/helpers/networkInfo.ts @@ -1,8 +1,8 @@ -import networkTypes from './networkTypes'; +import { Network } from '@/networks/types'; // TODO: networkInfo is DEPRECATED after the new network support changes const networkInfo = { - [`${networkTypes.mainnet}`]: { + [`${Network.mainnet}`]: { balance_checker_contract_address: '0x4dcf4562268dd384fe814c00fad239f06c2a0c2b', color: '#0E76FD', disabled: false, @@ -10,9 +10,9 @@ const networkInfo = { faucet_url: null, name: 'Ethereum', gasToken: 'ETH', - value: networkTypes.mainnet, + value: Network.mainnet, }, - [`${networkTypes.goerli}`]: { + [`${Network.goerli}`]: { balance_checker_contract_address: '0xf3352813b612a2d198e437691557069316b84ebe', color: '#f6c343', disabled: false, @@ -21,9 +21,9 @@ const networkInfo = { name: 'Goerli', gasToken: 'ETH', testnet: true, - value: networkTypes.goerli, + value: Network.goerli, }, - [`${networkTypes.arbitrum}`]: { + [`${Network.arbitrum}`]: { balance_checker_contract_address: '0x54A4E5800345c01455a7798E0D96438364e22723', color: '#2D374B', disabled: false, @@ -32,9 +32,9 @@ const networkInfo = { layer2: true, name: 'Arbitrum', gasToken: 'ETH', - value: networkTypes.arbitrum, + value: Network.arbitrum, }, - [`${networkTypes.optimism}`]: { + [`${Network.optimism}`]: { balance_checker_contract_address: '0x1C8cFdE3Ba6eFc4FF8Dd5C93044B9A690b6CFf36', color: '#FF4040', disabled: false, @@ -43,9 +43,9 @@ const networkInfo = { layer2: true, name: 'Optimism', gasToken: 'ETH', - value: networkTypes.optimism, + value: Network.optimism, }, - [`${networkTypes.polygon}`]: { + [`${Network.polygon}`]: { balance_checker_contract_address: '0x54A4E5800345c01455a7798E0D96438364e22723', color: '#8247E5', disabled: false, @@ -55,9 +55,9 @@ const networkInfo = { longName: 'Polygon (Matic)', name: 'Polygon', gasToken: 'MATIC', - value: networkTypes.polygon, + value: Network.polygon, }, - [`${networkTypes.bsc}`]: { + [`${Network.bsc}`]: { balance_checker_contract_address: '0x400A9f1Bb1Db80643C33710C2232A0D74EF5CFf1', color: '#F0B90B', disabled: false, @@ -67,7 +67,7 @@ const networkInfo = { longName: 'Binance Smart Chain', name: 'BSC', gasToken: 'BNB', - value: networkTypes.bsc, + value: Network.bsc, }, }; diff --git a/src/helpers/networkTypes.ts b/src/helpers/networkTypes.ts deleted file mode 100644 index 62e425cda73..00000000000 --- a/src/helpers/networkTypes.ts +++ /dev/null @@ -1,31 +0,0 @@ -export enum Network { - arbitrum = 'arbitrum', - goerli = 'goerli', - mainnet = 'mainnet', - optimism = 'optimism', - polygon = 'polygon', - base = 'base', - bsc = 'bsc', - zora = 'zora', - gnosis = 'gnosis', - avalanche = 'avalanche', - blast = 'blast', - degen = 'degen', -} - -// We need to keep this one until -// we have typescript everywhere -export default { - arbitrum: 'arbitrum' as Network, - goerli: 'goerli' as Network, - mainnet: 'mainnet' as Network, - optimism: 'optimism' as Network, - polygon: 'polygon' as Network, - base: 'base' as Network, - bsc: 'bsc' as Network, - zora: 'zora' as Network, - gnosis: 'gnosis' as Network, - avalanche: 'avalanche' as Network, - blast: 'blast' as Network, - degen: 'degen' as Network, -}; diff --git a/src/helpers/statusBarHelper.ts b/src/helpers/statusBarHelper.ts index 47d431a1feb..e8b5f72221e 100644 --- a/src/helpers/statusBarHelper.ts +++ b/src/helpers/statusBarHelper.ts @@ -1,5 +1,4 @@ import { ColorValue, StatusBar, StatusBarAnimation } from 'react-native'; -import { getSoftMenuBarHeight } from 'react-native-extra-dimensions-android'; export const setTranslucent = (translucent: boolean): void => { StatusBar.setTranslucent(translucent); @@ -30,7 +29,3 @@ export const setDarkContent = (isAnimated = true) => { barStyle: 'dark-content', }); }; - -export const isUsingButtonNavigation = () => { - return getSoftMenuBarHeight() > 95; -}; diff --git a/src/helpers/validators.ts b/src/helpers/validators.ts index 72aceda6c26..3016e5c7950 100644 --- a/src/helpers/validators.ts +++ b/src/helpers/validators.ts @@ -1,8 +1,8 @@ import { isValidAddress } from 'ethereumjs-util'; import { memoFn } from '../utils/memoFn'; -import { Network } from './networkTypes'; -import { getProviderForNetwork, isHexStringIgnorePrefix, isValidMnemonic, resolveUnstoppableDomain } from '@/handlers/web3'; +import { getProvider, isHexStringIgnorePrefix, isValidMnemonic, resolveUnstoppableDomain } from '@/handlers/web3'; import { sanitizeSeedPhrase } from '@/utils/formatters'; +import { ChainId } from '@/networks/types'; // Currently supported Top Level Domains from Unstoppable Domains const supportedUnstoppableDomains = ['888', 'bitcoin', 'blockchain', 'coin', 'crypto', 'dao', 'nft', 'wallet', 'x', 'zil']; @@ -68,7 +68,7 @@ export const checkIsValidAddressOrDomainFormat = (address: any) => { * @return {Boolean} */ export const checkIsValidAddressOrDomain = async (address: any) => { - const provider = getProviderForNetwork(Network.mainnet); + const provider = getProvider({ chainId: ChainId.mainnet }); if (isENSAddressFormat(address)) { try { const resolvedAddress = await provider.resolveName(address); diff --git a/src/helpers/walletConnectNetworks.ts b/src/helpers/walletConnectNetworks.ts index 326399fd897..cb02ab79f2c 100644 --- a/src/helpers/walletConnectNetworks.ts +++ b/src/helpers/walletConnectNetworks.ts @@ -1,12 +1,12 @@ -import { RainbowNetworks, getNetworkObj } from '@/networks'; -import { Network } from '@/networks/types'; +import { RainbowNetworkObjects, getNetworkObject } from '@/networks'; import store from '@/redux/store'; import { showActionSheetWithOptions } from '@/utils'; import * as i18n from '@/languages'; +import { ChainId } from '@/networks/types'; const androidNetworkActions = () => { const { testnetsEnabled } = store.getState().settings; - return RainbowNetworks.filter( + return RainbowNetworkObjects.filter( ({ features, networkType }) => features.walletconnect && (testnetsEnabled || networkType !== 'testnet') ).map(network => network.name); }; @@ -15,7 +15,7 @@ export const NETWORK_MENU_ACTION_KEY_FILTER = 'switch-to-network-'; export const networksMenuItems = () => { const { testnetsEnabled } = store.getState().settings; - return RainbowNetworks.filter( + return RainbowNetworkObjects.filter( ({ features, networkType }) => features.walletconnect && (testnetsEnabled || networkType !== 'testnet') ).map(network => ({ actionKey: `${NETWORK_MENU_ACTION_KEY_FILTER}${network.value}`, @@ -76,7 +76,8 @@ export const androidShowNetworksActionSheet = (callback: any) => { (idx: any) => { if (idx !== undefined) { const networkActions = androidNetworkActions(); - const networkObj = RainbowNetworks.find(network => network.name === networkActions[idx]) || getNetworkObj(Network.mainnet); + const networkObj = + RainbowNetworkObjects.find(network => network.name === networkActions[idx]) || getNetworkObject({ chainId: ChainId.mainnet }); callback({ chainId: networkObj.id, network: networkObj.value }); } } diff --git a/src/hooks/charts/useChartInfo.ts b/src/hooks/charts/useChartInfo.ts index 05efb97f142..e2402eb2e9f 100644 --- a/src/hooks/charts/useChartInfo.ts +++ b/src/hooks/charts/useChartInfo.ts @@ -1,18 +1,11 @@ import { useNavigation, useRoute } from '@react-navigation/native'; -import { isEmpty } from 'lodash'; -import { useCallback, useEffect, useState } from 'react'; -import isEqual from 'react-fast-compare'; -import { useDispatch, useSelector } from 'react-redux'; -import { createSelector } from 'reselect'; -import { useCallbackOne } from 'use-memo-one'; -import { disableCharts } from '../../config/debug'; +import { useCallback } from 'react'; import { DEFAULT_CHART_TYPE } from '../../redux/charts'; import { metadataClient } from '@/graphql'; import { useQuery } from '@tanstack/react-query'; import { createQueryKey } from '@/react-query'; -import { getNetworkObj } from '@/networks'; -import { NetworkProperties } from '@/networks/types'; -import { Network } from '@/helpers'; +import { SupportedCurrencyKey } from '@/references'; +import { ChainId } from '@/networks/types'; const chartTimes = ['hour', 'day', 'week', 'month', 'year'] as const; type ChartTime = (typeof chartTimes)[number]; @@ -23,9 +16,19 @@ const getChartTimeArg = (selected: ChartTime) => export type ChartData = { x: number; y: number }; -const fetchPriceChart = async (time: ChartTime, chainId: NetworkProperties['id'], address: string) => { +const fetchPriceChart = async ({ + address, + chainId, + currency, + time, +}: { + address: string; + chainId: ChainId; + currency: SupportedCurrencyKey; + time: ChartTime; +}) => { const priceChart = await metadataClient - .priceChart({ address, chainId, ...getChartTimeArg(time) }) + .priceChart({ address, chainId, currency, ...getChartTimeArg(time) }) .then(d => d.token?.priceCharts[time] as PriceChartTimeData); return priceChart?.points?.reduce((result, point) => { result.push({ x: point[0], y: point[1] }); @@ -33,7 +36,17 @@ const fetchPriceChart = async (time: ChartTime, chainId: NetworkProperties['id'] }, [] as ChartData[]); }; -export const usePriceChart = ({ mainnetAddress, address, network }: { mainnetAddress?: string; address: string; network: Network }) => { +export const usePriceChart = ({ + mainnetAddress, + address, + currency, + chainId, +}: { + mainnetAddress?: string; + address: string; + currency: SupportedCurrencyKey; + chainId: ChainId; +}) => { const { setParams } = useNavigation(); const updateChartType = useCallback( (type: ChartTime) => { @@ -47,12 +60,11 @@ export const usePriceChart = ({ mainnetAddress, address, network }: { mainnetAdd params: any; }>(); const chartType = params?.chartType ?? DEFAULT_CHART_TYPE; - const chainId = getNetworkObj(network).id; - const mainnetChainId = getNetworkObj(Network.mainnet).id; const query = useQuery({ queryFn: async () => { - const chart = await fetchPriceChart(chartType, chainId, address); - if (!chart && mainnetAddress) return fetchPriceChart(chartType, mainnetChainId, mainnetAddress); + const chart = await fetchPriceChart({ address, chainId, currency, time: chartType }); + if (!chart && mainnetAddress) + return fetchPriceChart({ address: mainnetAddress, chainId: ChainId.mainnet, currency, time: chartType }); return chart || null; }, queryKey: createQueryKey('price chart', { address, chainId, chartType }), diff --git a/src/hooks/charts/useChartThrottledPoints.ts b/src/hooks/charts/useChartThrottledPoints.ts index a88dc3b111c..3586efe49c6 100644 --- a/src/hooks/charts/useChartThrottledPoints.ts +++ b/src/hooks/charts/useChartThrottledPoints.ts @@ -98,8 +98,9 @@ export default function useChartThrottledPoints({ updateChartType, } = usePriceChart({ address: asset.address, - network: asset.network, + chainId: asset.chainId, mainnetAddress: asset?.mainnet_address || asset?.mainnetAddress, + currency: nativeCurrency, }); const [throttledPoints, setThrottledPoints] = useState(() => traverseData({ nativePoints: [], points: [] }, chart)); diff --git a/src/hooks/useAccountAsset.ts b/src/hooks/useAccountAsset.ts index 146e6412ff9..2d2bccc2699 100644 --- a/src/hooks/useAccountAsset.ts +++ b/src/hooks/useAccountAsset.ts @@ -1,9 +1,10 @@ +import { NativeCurrencyKey } from '@/entities'; import useAccountSettings from './useAccountSettings'; import { parseAssetNative } from '@/parsers'; import { useUserAsset } from '@/resources/assets/useUserAsset'; // this is meant to be used for assets contained in the current wallet -export default function useAccountAsset(uniqueId: string, nativeCurrency: string | undefined = undefined) { +export default function useAccountAsset(uniqueId: string, nativeCurrency: NativeCurrencyKey | undefined = undefined) { const { data: accountAsset } = useUserAsset(uniqueId); // this is temporary for FastBalanceCoinRow to make a tiny bit faster diff --git a/src/hooks/useAccountTransactions.ts b/src/hooks/useAccountTransactions.ts index f7dc48cdac1..9fab4b41ae9 100644 --- a/src/hooks/useAccountTransactions.ts +++ b/src/hooks/useAccountTransactions.ts @@ -8,9 +8,9 @@ import { useTheme } from '@/theme'; import { useConsolidatedTransactions } from '@/resources/transactions/consolidatedTransactions'; import { RainbowTransaction } from '@/entities'; import { pendingTransactionsStore, usePendingTransactionsStore } from '@/state/pendingTransactions'; -import { RainbowNetworks } from '@/networks'; -import { Network } from '@/networks/types'; +import { RainbowNetworkObjects } from '@/networks'; import { nonceStore } from '@/state/nonces'; +import { ChainId } from '@/networks/types'; export const NOE_PAGE = 30; @@ -34,16 +34,16 @@ export default function useAccountTransactions() { .filter(t => t.from?.toLowerCase() === accountAddress?.toLowerCase()) .reduce( (latestTxMap, currentTx) => { - const currentNetwork = currentTx?.network; - if (currentNetwork) { - const latestTx = latestTxMap.get(currentNetwork); + const currentChainId = currentTx?.chainId; + if (currentChainId) { + const latestTx = latestTxMap.get(currentChainId); if (!latestTx) { - latestTxMap.set(currentNetwork, currentTx); + latestTxMap.set(currentChainId, currentTx); } } return latestTxMap; }, - new Map(RainbowNetworks.map(chain => [chain.value, null as RainbowTransaction | null])) + new Map(RainbowNetworkObjects.map(chain => [chain.id, null as RainbowTransaction | null])) ); watchForPendingTransactionsReportedByRainbowBackend({ currentAddress: accountAddress, @@ -56,17 +56,17 @@ export default function useAccountTransactions() { latestTransactions, }: { currentAddress: string; - latestTransactions: Map; + latestTransactions: Map; }) { const { setNonce } = nonceStore.getState(); const { setPendingTransactions, pendingTransactions: storePendingTransactions } = pendingTransactionsStore.getState(); const pendingTransactions = storePendingTransactions[currentAddress] || []; - const networks = RainbowNetworks.filter(({ enabled, networkType }) => enabled && networkType !== 'testnet'); + const networks = RainbowNetworkObjects.filter(({ enabled, networkType }) => enabled && networkType !== 'testnet'); for (const network of networks) { - const latestTxConfirmedByBackend = latestTransactions.get(network.value); + const latestTxConfirmedByBackend = latestTransactions.get(network.id); if (latestTxConfirmedByBackend) { const latestNonceConfirmedByBackend = latestTxConfirmedByBackend.nonce || 0; - const [latestPendingTx] = pendingTransactions.filter(tx => tx?.network === network.value); + const [latestPendingTx] = pendingTransactions.filter(tx => tx?.chainId === network.id); let currentNonce; if (latestPendingTx) { @@ -79,7 +79,7 @@ export default function useAccountTransactions() { setNonce({ address: currentAddress, - network: network.value, + chainId: network.id, currentNonce, latestConfirmedNonce: latestNonceConfirmedByBackend, }); @@ -88,7 +88,7 @@ export default function useAccountTransactions() { const updatedPendingTransactions = pendingTransactions?.filter(tx => { const txNonce = tx.nonce || 0; - const latestTx = latestTransactions.get(tx.network); + const latestTx = latestTransactions.get(tx.chainId); const latestTxNonce = latestTx?.nonce || 0; // still pending or backend is not returning confirmation yet // if !latestTx means that is the first tx of the wallet diff --git a/src/hooks/useAdditionalAssetData.ts b/src/hooks/useAdditionalAssetData.ts index cf8b8569890..df16a1dcd74 100644 --- a/src/hooks/useAdditionalAssetData.ts +++ b/src/hooks/useAdditionalAssetData.ts @@ -1,9 +1,8 @@ import { useQuery } from '@tanstack/react-query'; import { NativeCurrencyKey } from '@/entities'; -import { Network } from '@/networks/types'; import { metadataClient } from '@/graphql'; -import { ethereumUtils } from '@/utils'; import { Token } from '@/graphql/__generated__/metadata'; +import { ChainId } from '@/networks/types'; // Types type TokenMetadata = Pick< @@ -14,21 +13,20 @@ type TokenMetadata = Pick< // Types for the query arguments type AdditionalAssetDataArgs = { address: string; - network: Network; + chainId: ChainId; currency: NativeCurrencyKey; }; // Query Key function -const createAdditionalAssetDataQueryKey = ({ address, network, currency }: AdditionalAssetDataArgs) => [ +const createAdditionalAssetDataQueryKey = ({ address, chainId, currency }: AdditionalAssetDataArgs) => [ 'additionalAssetData', address, - network, + chainId, currency, ]; // Refactor the getAdditionalAssetData function to accept the new parameters -async function getAdditionalAssetData({ address, network, currency }: AdditionalAssetDataArgs): Promise { - const chainId = ethereumUtils.getChainIdFromNetwork(network); +async function getAdditionalAssetData({ address, chainId, currency }: AdditionalAssetDataArgs): Promise { const data = await metadataClient.tokenMetadata({ address, chainId, @@ -42,12 +40,12 @@ async function getAdditionalAssetData({ address, network, currency }: Additional } // Usage of the useQuery hook -export default function useAdditionalAssetData({ address, network, currency }: AdditionalAssetDataArgs) { +export default function useAdditionalAssetData({ address, chainId, currency }: AdditionalAssetDataArgs) { return useQuery( - createAdditionalAssetDataQueryKey({ address, network, currency }), - () => getAdditionalAssetData({ address, network, currency }), + createAdditionalAssetDataQueryKey({ address, chainId, currency }), + () => getAdditionalAssetData({ address, chainId, currency }), { - enabled: !!address && !!network && !!currency, // Ensure all parameters are provided + enabled: !!address && !!chainId && !!currency, // Ensure all parameters are provided } ); } diff --git a/src/hooks/useApplicationSetup.ts b/src/hooks/useApplicationSetup.ts new file mode 100644 index 00000000000..0ce196b74b8 --- /dev/null +++ b/src/hooks/useApplicationSetup.ts @@ -0,0 +1,56 @@ +import { useCallback, useEffect, useState } from 'react'; +import { logger, RainbowError } from '@/logger'; +import { InteractionManager } from 'react-native'; +import { handleReviewPromptAction } from '@/utils/reviewAlert'; +import { ReviewPromptAction } from '@/storage/schema'; +import { loadAddress } from '@/model/wallet'; +import { InitialRoute } from '@/navigation/initialRoute'; +import { PerformanceContextMap } from '@/performance/PerformanceContextMap'; +import Routes from '@/navigation/routesNames'; +import { checkIdentifierOnLaunch } from '@/model/backup'; +import { analyticsV2 } from '@/analytics'; +import { saveFCMToken } from '@/notifications/tokens'; +import { initListeners as initWalletConnectListeners, initWalletConnectPushNotifications } from '@/walletConnect'; +import isTestFlight from '@/helpers/isTestFlight'; +import { PerformanceTracking } from '@/performance/tracking'; +import { PerformanceMetrics } from '@/performance/tracking/types/PerformanceMetrics'; + +export function useApplicationSetup() { + const [initialRoute, setInitialRoute] = useState(null); + + const identifyFlow = useCallback(async () => { + const address = await loadAddress(); + if (address) { + setTimeout(() => { + InteractionManager.runAfterInteractions(() => { + handleReviewPromptAction(ReviewPromptAction.TimesLaunchedSinceInstall); + }); + }, 10_000); + + InteractionManager.runAfterInteractions(checkIdentifierOnLaunch); + } + + setInitialRoute(address ? Routes.SWIPE_LAYOUT : Routes.WELCOME_SCREEN); + PerformanceContextMap.set('initialRoute', address ? Routes.SWIPE_LAYOUT : Routes.WELCOME_SCREEN); + }, []); + + useEffect(() => { + if (!IS_DEV && isTestFlight) { + logger.debug(`[App]: Test flight usage - ${isTestFlight}`); + } + identifyFlow(); + initWalletConnectListeners(); + + Promise.all([analyticsV2.initializeRudderstack(), saveFCMToken()]) + .catch(error => { + logger.error(new RainbowError('Failed to initialize rudderstack or save FCM token', error)); + }) + .finally(() => { + initWalletConnectPushNotifications(); + PerformanceTracking.finishMeasuring(PerformanceMetrics.loadRootAppComponent); + analyticsV2.track(analyticsV2.event.applicationDidMount); + }); + }, [identifyFlow]); + + return { initialRoute }; +} diff --git a/src/hooks/useAsset.ts b/src/hooks/useAsset.ts index 9a5e3180397..91a77b25563 100644 --- a/src/hooks/useAsset.ts +++ b/src/hooks/useAsset.ts @@ -4,11 +4,12 @@ import { getUniqueId } from '@/utils/ethereumUtils'; import { useExternalToken } from '@/resources/assets/externalAssetsQuery'; import { useSelector } from 'react-redux'; import { AppState } from '@/redux/store'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; +import { Address } from 'viem'; // To fetch an asset from account assets, // generic assets, and uniqueTokens -export default function useAsset({ address, chainId }: { address: string; chainId: ChainId }) { +export default function useAsset({ address, chainId }: { address: Address; chainId: ChainId }) { const nativeCurrency = useSelector((state: AppState) => state.settings.nativeCurrency); const uniqueId = getUniqueId(address, chainId); const accountAsset = useAccountAsset(uniqueId); diff --git a/src/hooks/useCalculateGasLimit.ts b/src/hooks/useCalculateGasLimit.ts new file mode 100644 index 00000000000..1ae0800caae --- /dev/null +++ b/src/hooks/useCalculateGasLimit.ts @@ -0,0 +1,74 @@ +import { useEffect, useRef, useCallback } from 'react'; +import { estimateGas, web3Provider, toHex } from '@/handlers/web3'; +import { convertHexToString, omitFlatten } from '@/helpers/utilities'; +import { logger, RainbowError } from '@/logger'; +import { getNetworkObject } from '@/networks'; +import { ethereumUtils } from '@/utils'; +import { hexToNumber, isHex } from 'viem'; +import { isEmpty } from 'lodash'; +import { InteractionManager } from 'react-native'; +import { GasFeeParamsBySpeed } from '@/entities'; +import { StaticJsonRpcProvider } from '@ethersproject/providers'; +import { useGas } from '@/hooks'; +import { ChainId } from '@/networks/types'; + +type CalculateGasLimitProps = { + isMessageRequest: boolean; + gasFeeParamsBySpeed: GasFeeParamsBySpeed; + provider: StaticJsonRpcProvider | null; + req: any; + updateTxFee: ReturnType['updateTxFee']; + chainId: ChainId; +}; + +export const useCalculateGasLimit = ({ + isMessageRequest, + gasFeeParamsBySpeed, + provider, + req, + updateTxFee, + chainId, +}: CalculateGasLimitProps) => { + const calculatingGasLimit = useRef(false); + + const calculateGasLimit = useCallback(async () => { + calculatingGasLimit.current = true; + const txPayload = req; + if (isHex(txPayload?.type)) { + txPayload.type = hexToNumber(txPayload?.type); + } + let gas = txPayload.gasLimit || txPayload.gas; + + try { + logger.debug('WC: Estimating gas limit', { gas }, logger.DebugContext.walletconnect); + const cleanTxPayload = omitFlatten(txPayload, ['gas', 'gasLimit', 'gasPrice', 'maxFeePerGas', 'maxPriorityFeePerGas']); + const rawGasLimit = await estimateGas(cleanTxPayload, provider); + logger.debug('WC: Estimated gas limit', { rawGasLimit }, logger.DebugContext.walletconnect); + if (rawGasLimit) { + gas = toHex(rawGasLimit); + } + } catch (error) { + logger.error(new RainbowError('WC: error estimating gas'), { error }); + } finally { + logger.debug('WC: Setting gas limit to', { gas: convertHexToString(gas) }, logger.DebugContext.walletconnect); + + const networkObject = getNetworkObject({ chainId }); + if (chainId && networkObject.gas.OptimismTxFee) { + const l1GasFeeOptimism = await ethereumUtils.calculateL1FeeOptimism(txPayload, provider || web3Provider); + updateTxFee(gas, null, l1GasFeeOptimism); + } else { + updateTxFee(gas, null); + } + } + }, [chainId, req, updateTxFee, provider]); + + useEffect(() => { + if (!isEmpty(gasFeeParamsBySpeed) && !calculatingGasLimit.current && !isMessageRequest && provider) { + InteractionManager.runAfterInteractions(() => { + calculateGasLimit(); + }); + } + }, [calculateGasLimit, gasFeeParamsBySpeed, isMessageRequest, provider]); + + return { calculateGasLimit }; +}; diff --git a/src/hooks/useClipboard.ts b/src/hooks/useClipboard.ts index 5d01aedd768..ede5478387d 100644 --- a/src/hooks/useClipboard.ts +++ b/src/hooks/useClipboard.ts @@ -1,5 +1,5 @@ import Clipboard from '@react-native-clipboard/clipboard'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useLayoutEffect, useState } from 'react'; import useAppState from './useAppState'; import { deviceUtils } from '@/utils'; @@ -27,7 +27,7 @@ export default function useClipboard() { ); // Get initial clipboardData - useEffect(() => { + useLayoutEffect(() => { if (deviceUtils.isIOS14) { checkClipboard(); } else if (!deviceUtils.hasClipboardProtection) { @@ -60,7 +60,7 @@ export default function useClipboard() { clipboard: clipboardData, enablePaste: deviceUtils.isIOS14 ? hasClipboardData : deviceUtils.hasClipboardProtection || !!clipboardData, getClipboard, - hasClipboardData, + hasClipboardData: hasClipboardData || !!clipboardData, setClipboard, }; } diff --git a/src/hooks/useConfirmTransaction.ts b/src/hooks/useConfirmTransaction.ts new file mode 100644 index 00000000000..6600cead84b --- /dev/null +++ b/src/hooks/useConfirmTransaction.ts @@ -0,0 +1,27 @@ +import { useCallback } from 'react'; + +type UseConfirmTransactionProps = { + isMessageRequest: boolean; + isBalanceEnough: boolean | undefined; + isValidGas: boolean; + handleSignMessage: () => void; + handleConfirmTransaction: () => void; +}; + +export const useConfirmTransaction = ({ + isMessageRequest, + isBalanceEnough, + isValidGas, + handleSignMessage, + handleConfirmTransaction, +}: UseConfirmTransactionProps) => { + const onConfirm = useCallback(async () => { + if (isMessageRequest) { + return handleSignMessage(); + } + if (!isBalanceEnough || !isValidGas) return; + return handleConfirmTransaction(); + }, [isMessageRequest, isBalanceEnough, isValidGas, handleConfirmTransaction, handleSignMessage]); + + return { onConfirm }; +}; diff --git a/src/hooks/useContacts.ts b/src/hooks/useContacts.ts index 277a5f77e1c..73aaac697f7 100644 --- a/src/hooks/useContacts.ts +++ b/src/hooks/useContacts.ts @@ -2,7 +2,6 @@ import { sortBy, values } from 'lodash'; import { useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { createSelector } from 'reselect'; -import networkTypes from '../helpers/networkTypes'; import { contactsAddOrUpdate, removeContact } from '../redux/contacts'; import { AppState } from '@/redux/store'; @@ -16,9 +15,6 @@ const contactsSelector = createSelector( export default function useContacts() { const dispatch = useDispatch(); - const { network } = useSelector(({ settings: { network } }: AppState) => ({ - network, - })); const { contacts, sortedContacts } = useSelector(contactsSelector); const onAddOrUpdateContacts = useCallback( @@ -29,13 +25,9 @@ export default function useContacts() { const onRemoveContact = useCallback((data: string) => dispatch(removeContact(data)), [dispatch]); - const filteredContacts = sortedContacts.filter(contact => - contact.network === network || (!contact.network && network === networkTypes.mainnet) ? contact : false - ); - return { contacts, - filteredContacts, + filteredContacts: sortedContacts, onAddOrUpdateContacts, onRemoveContact, sortedContacts, diff --git a/src/hooks/useENSRegistrationActionHandler.ts b/src/hooks/useENSRegistrationActionHandler.ts index 473f059b441..6216645ff4c 100644 --- a/src/hooks/useENSRegistrationActionHandler.ts +++ b/src/hooks/useENSRegistrationActionHandler.ts @@ -12,20 +12,20 @@ import { Records, RegistrationParameters } from '@/entities'; import { fetchResolver } from '@/handlers/ens'; import { saveNameFromLabelhash } from '@/handlers/localstorage/ens'; import { uploadImage } from '@/handlers/pinata'; -import { getProviderForNetwork } from '@/handlers/web3'; +import { getProvider } from '@/handlers/web3'; import { ENS_DOMAIN, generateSalt, getRentPrice, REGISTRATION_STEPS } from '@/helpers/ens'; import { loadWallet } from '@/model/wallet'; import { timeUnits } from '@/references'; import Routes from '@/navigation/routesNames'; import { labelhash } from '@/utils'; import { getNextNonce } from '@/state/nonces'; -import { Network } from '@/networks/types'; import { Hex } from 'viem'; import { executeENSRap } from '@/raps/actions/ens'; import store from '@/redux/store'; import { performanceTracking, Screens, TimeToSignOperation } from '@/state/performance/performance'; import { noop } from 'lodash'; import { logger, RainbowError } from '@/logger'; +import { ChainId } from '@/networks/types'; // Generic type for action functions type ActionFunction

= (...params: P) => Promise; @@ -115,7 +115,7 @@ const useENSRegistrationActionHandler: UseENSRegistrationActionHandler = ({ step }; (() => { - provider = getProviderForNetwork(); + provider = getProvider({ chainId: ChainId.mainnet }); provider.on('block', updateAvatars); })(); return () => { @@ -128,7 +128,7 @@ const useENSRegistrationActionHandler: UseENSRegistrationActionHandler = ({ step async (callback = noop) => { updateAvatarsOnNextBlock.current = true; - const provider = getProviderForNetwork(); + const provider = getProvider({ chainId: ChainId.mainnet }); const wallet = await loadWallet({ showErrorIfNotLoaded: false, provider, @@ -139,7 +139,7 @@ const useENSRegistrationActionHandler: UseENSRegistrationActionHandler = ({ step const salt = generateSalt(); const [nonce, rentPrice] = await Promise.all([ - getNextNonce({ network: Network.mainnet, address: accountAddress }), + getNextNonce({ chainId: ChainId.mainnet, address: accountAddress }), getRentPrice(registrationParameters.name.replace(ENS_DOMAIN, ''), duration), ]); @@ -186,7 +186,7 @@ const useENSRegistrationActionHandler: UseENSRegistrationActionHandler = ({ step async (callback = noop) => { const { name, duration } = registrationParameters as RegistrationParameters; - const provider = getProviderForNetwork(); + const provider = getProvider({ chainId: ChainId.mainnet }); const wallet = await loadWallet({ showErrorIfNotLoaded: false, provider, @@ -196,7 +196,7 @@ const useENSRegistrationActionHandler: UseENSRegistrationActionHandler = ({ step } const [nonce, rentPrice, changedRecords] = await Promise.all([ - getNextNonce({ network: Network.mainnet, address: accountAddress }), + getNextNonce({ chainId: ChainId.mainnet, address: accountAddress }), getRentPrice(name.replace(ENS_DOMAIN, ''), duration), uploadRecordImages(registrationParameters.changedRecords, { avatar: avatarMetadata, @@ -225,7 +225,7 @@ const useENSRegistrationActionHandler: UseENSRegistrationActionHandler = ({ step async (callback = noop) => { const { name } = registrationParameters as RegistrationParameters; - const provider = getProviderForNetwork(); + const provider = getProvider({ chainId: ChainId.mainnet }); const wallet = await loadWallet({ showErrorIfNotLoaded: false, provider, @@ -234,7 +234,7 @@ const useENSRegistrationActionHandler: UseENSRegistrationActionHandler = ({ step return; } - const nonce = await getNextNonce({ network: Network.mainnet, address: accountAddress }); + const nonce = await getNextNonce({ chainId: ChainId.mainnet, address: accountAddress }); const rentPrice = await getRentPrice(name.replace(ENS_DOMAIN, ''), duration); const registerEnsRegistrationParameters: ENSActionParameters = { @@ -253,7 +253,7 @@ const useENSRegistrationActionHandler: UseENSRegistrationActionHandler = ({ step async (callback = noop) => { const { name } = registrationParameters as RegistrationParameters; - const provider = getProviderForNetwork(); + const provider = getProvider({ chainId: ChainId.mainnet }); const wallet = await loadWallet({ showErrorIfNotLoaded: false, provider, @@ -262,7 +262,7 @@ const useENSRegistrationActionHandler: UseENSRegistrationActionHandler = ({ step return; } - const nonce = await getNextNonce({ network: Network.mainnet, address: accountAddress }); + const nonce = await getNextNonce({ chainId: ChainId.mainnet, address: accountAddress }); const registerEnsRegistrationParameters: ENSActionParameters = { ...formatENSActionParams(registrationParameters), @@ -278,7 +278,7 @@ const useENSRegistrationActionHandler: UseENSRegistrationActionHandler = ({ step const setRecordsAction: ActionTypes[typeof REGISTRATION_STEPS.EDIT] = useCallback( async (callback = noop) => { - const provider = getProviderForNetwork(); + const provider = getProvider({ chainId: ChainId.mainnet }); const wallet = await loadWallet({ showErrorIfNotLoaded: false, provider, @@ -288,7 +288,7 @@ const useENSRegistrationActionHandler: UseENSRegistrationActionHandler = ({ step } const [nonce, changedRecords, resolver] = await Promise.all([ - getNextNonce({ network: Network.mainnet, address: accountAddress }), + getNextNonce({ chainId: ChainId.mainnet, address: accountAddress }), uploadRecordImages(registrationParameters.changedRecords, { avatar: avatarMetadata, header: coverMetadata, @@ -316,7 +316,7 @@ const useENSRegistrationActionHandler: UseENSRegistrationActionHandler = ({ step async ({ clearRecords, records, name, setAddress, toAddress, transferControl, wallet: walletOverride }, callback = noop) => { let wallet = walletOverride; if (!wallet) { - const provider = getProviderForNetwork(); + const provider = getProvider({ chainId: ChainId.mainnet }); wallet = await loadWallet({ showErrorIfNotLoaded: false, provider, @@ -326,7 +326,7 @@ const useENSRegistrationActionHandler: UseENSRegistrationActionHandler = ({ step return; } - const nonce = await getNextNonce({ network: Network.mainnet, address: accountAddress }); + const nonce = await getNextNonce({ chainId: ChainId.mainnet, address: accountAddress }); const transferEnsParameters: ENSActionParameters = { ...formatENSActionParams({ diff --git a/src/hooks/useENSRegistrationCosts.ts b/src/hooks/useENSRegistrationCosts.ts index d1143975b3e..75fc975a023 100644 --- a/src/hooks/useENSRegistrationCosts.ts +++ b/src/hooks/useENSRegistrationCosts.ts @@ -24,11 +24,10 @@ import { REGISTRATION_MODES, REGISTRATION_STEPS, } from '@/helpers/ens'; -import { Network } from '@/helpers/networkTypes'; import { add, addBuffer, addDisplay, fromWei, greaterThanOrEqualTo, multiply } from '@/helpers/utilities'; import { ethUnits, timeUnits } from '@/references'; import { ethereumUtils, gasUtils } from '@/utils'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; enum QUERY_KEYS { GET_COMMIT_GAS_LIMIT = 'GET_COMMIT_GAS_LIMIT', @@ -93,7 +92,7 @@ export default function useENSRegistrationCosts({ const rentPriceInWei = rentPrice?.wei?.toString(); const checkIfSufficientEth = useCallback((wei: string) => { - const nativeAsset = ethereumUtils.getNetworkNativeAsset(ChainId.mainnet); + const nativeAsset = ethereumUtils.getNetworkNativeAsset({ chainId: ChainId.mainnet }); const balanceAmount = nativeAsset?.balance?.amount || 0; const txFeeAmount = fromWei(wei); const isSufficientGas = greaterThanOrEqualTo(balanceAmount, txFeeAmount); @@ -248,7 +247,7 @@ export default function useENSRegistrationCosts({ ); const estimatedFee = useMemo(() => { - const nativeAssetPrice = ethereumUtils.getPriceOfNativeAssetForNetwork(Network.mainnet); + const nativeAssetPrice = ethereumUtils.getPriceOfNativeAssetForNetwork({ chainId: ChainId.mainnet }); const { gasFeeParamsBySpeed, currentBaseFee } = gasFeeParams; let estimatedGasLimit = ''; @@ -334,7 +333,7 @@ export default function useENSRegistrationCosts({ const data = useMemo(() => { const rentPricePerYearInWei = rentPrice?.perYear?.wei?.toString(); - const nativeAssetPrice = ethereumUtils.getPriceOfNativeAssetForNetwork(Network.mainnet); + const nativeAssetPrice = ethereumUtils.getPriceOfNativeAssetForNetwork({ chainId: ChainId.mainnet }); if (rentPricePerYearInWei) { const rentPriceInWei = multiply(rentPricePerYearInWei, yearsDuration); diff --git a/src/hooks/useENSRegistrationStepHandler.tsx b/src/hooks/useENSRegistrationStepHandler.tsx index 77653dca970..f53c8ece33a 100644 --- a/src/hooks/useENSRegistrationStepHandler.tsx +++ b/src/hooks/useENSRegistrationStepHandler.tsx @@ -4,7 +4,7 @@ import { useDispatch } from 'react-redux'; import usePrevious from './usePrevious'; import { useENSRegistration, useInterval } from '.'; import { RegistrationParameters } from '@/entities'; -import { getProviderForNetwork, isHardHat, web3Provider } from '@/handlers/web3'; +import { getProvider } from '@/handlers/web3'; import { ENS_SECONDS_PADDING, ENS_SECONDS_WAIT, @@ -14,18 +14,20 @@ import { REGISTRATION_STEPS, } from '@/helpers/ens'; import { updateTransactionRegistrationParameters } from '@/redux/ensRegistration'; +import { ChainId } from '@/networks/types'; +import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; const checkRegisterBlockTimestamp = async ({ registrationParameters, secondsSinceCommitConfirmed, - isTestingHardhat, + connectedToHardhat, }: { registrationParameters: RegistrationParameters; secondsSinceCommitConfirmed: number; - isTestingHardhat: boolean; + connectedToHardhat: boolean; }) => { try { - const provider = getProviderForNetwork(); + const provider = getProvider({ chainId: ChainId.mainnet }); const block = await provider.getBlock('latest'); const msBlockTimestamp = getBlockMsTimestamp(block); const secs = differenceInSeconds(msBlockTimestamp, registrationParameters?.commitTransactionConfirmedAt || msBlockTimestamp); @@ -33,7 +35,7 @@ const checkRegisterBlockTimestamp = async ({ (secs > ENS_SECONDS_WAIT_WITH_PADDING && secondsSinceCommitConfirmed > ENS_SECONDS_WAIT_WITH_PADDING) || // sometimes the provider.getBlock('latest) takes a long time to update to newest block secondsSinceCommitConfirmed > ENS_SECONDS_WAIT_PROVIDER_PADDING || - isTestingHardhat + connectedToHardhat ) { return true; } @@ -60,12 +62,12 @@ export default function useENSRegistrationStepHandler(observer = true) { -1 ); - const isTestingHardhat = useMemo(() => isHardHat(web3Provider.connection.url), []); + const { connectedToHardhat } = useConnectedToHardhatStore(); const [readyToRegister, setReadyToRegister] = useState(secondsSinceCommitConfirmed > ENS_SECONDS_WAIT); // flag to wait 10 secs before we get the tx block, to be able to simulate not confirmed tx when testing - const shouldLoopForConfirmation = useRef(isTestingHardhat); + const shouldLoopForConfirmation = useRef(connectedToHardhat); const registrationStep = useMemo(() => { if (mode === REGISTRATION_MODES.EDIT) return REGISTRATION_STEPS.EDIT; @@ -90,7 +92,7 @@ export default function useENSRegistrationStepHandler(observer = true) { const watchCommitTransaction = useCallback(async () => { if (observer) return; - const provider = getProviderForNetwork(); + const provider = getProvider({ chainId: ChainId.mainnet }); let confirmed = false; const tx = await provider.getTransaction(commitTransactionHash || ''); if (!tx?.blockHash) return confirmed; @@ -99,7 +101,7 @@ export default function useENSRegistrationStepHandler(observer = true) { const now = Date.now(); const msBlockTimestamp = getBlockMsTimestamp(block); // hardhat block timestamp is behind - const timeDifference = isTestingHardhat ? now - msBlockTimestamp : 0; + const timeDifference = connectedToHardhat ? now - msBlockTimestamp : 0; const commitTransactionConfirmedAt = msBlockTimestamp + timeDifference; const secs = differenceInSeconds(now, commitTransactionConfirmedAt); setSecondsSinceCommitConfirmed(secondsSinceCommitConfirmed < 0 ? 0 : secs); @@ -113,7 +115,7 @@ export default function useENSRegistrationStepHandler(observer = true) { shouldLoopForConfirmation.current = false; } return confirmed; - }, [observer, commitTransactionHash, isTestingHardhat, secondsSinceCommitConfirmed, dispatch]); + }, [observer, commitTransactionHash, connectedToHardhat, secondsSinceCommitConfirmed, dispatch]); const startPollingWatchCommitTransaction = useCallback(async () => { if (observer) return; @@ -166,7 +168,7 @@ export default function useENSRegistrationStepHandler(observer = true) { if (!observer && secondsSinceCommitConfirmed % 2 === 0 && secondsSinceCommitConfirmed >= ENS_SECONDS_WAIT && !readyToRegister) { const checkIfReadyToRegister = async () => { const readyToRegister = await checkRegisterBlockTimestamp({ - isTestingHardhat, + connectedToHardhat, registrationParameters, secondsSinceCommitConfirmed, }); @@ -174,7 +176,7 @@ export default function useENSRegistrationStepHandler(observer = true) { }; checkIfReadyToRegister(); } - }, [isTestingHardhat, observer, readyToRegister, registrationParameters, secondsSinceCommitConfirmed]); + }, [connectedToHardhat, observer, readyToRegister, registrationParameters, secondsSinceCommitConfirmed]); useEffect( () => () => { diff --git a/src/hooks/useENSSearch.ts b/src/hooks/useENSSearch.ts index 29cc46b6722..b492ad0749f 100644 --- a/src/hooks/useENSSearch.ts +++ b/src/hooks/useENSSearch.ts @@ -4,9 +4,9 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { useAccountSettings, useENSLocalTransactions } from '.'; import { fetchRegistrationDate } from '@/handlers/ens'; import { ENS_DOMAIN, formatRentPrice, getAvailable, getENSRegistrarControllerContract, getNameExpires, getRentPrice } from '@/helpers/ens'; -import { Network } from '@/helpers/networkTypes'; import { timeUnits } from '@/references'; import { ethereumUtils, validateENS } from '@/utils'; +import { ChainId } from '@/networks/types'; const formatTime = (timestamp: string, abbreviated = true) => { const style = abbreviated ? 'MMM d, y' : 'MMMM d, y'; @@ -51,7 +51,7 @@ export default function useENSSearch({ yearsDuration = 1, name: inputName }: { y } const [isAvailable, rentPrice] = await Promise.all([getAvailable(name, contract), getRentPrice(name, duration, contract)]); - const nativeAssetPrice = ethereumUtils.getPriceOfNativeAssetForNetwork(Network.mainnet); + const nativeAssetPrice = ethereumUtils.getPriceOfNativeAssetForNetwork({ chainId: ChainId.mainnet }); const formattedRentPrice = formatRentPrice(rentPrice, yearsDuration, nativeCurrency, nativeAssetPrice); if (isAvailable) { diff --git a/src/hooks/useGas.ts b/src/hooks/useGas.ts index 224e25ebc8e..4c6c7f3c2ce 100644 --- a/src/hooks/useGas.ts +++ b/src/hooks/useGas.ts @@ -13,7 +13,6 @@ import { ParsedAddressAsset, SelectedGasFee, } from '@/entities'; -import networkTypes, { Network } from '@/helpers/networkTypes'; import { fromWei, greaterThan, greaterThanOrEqualTo } from '@/helpers/utilities'; import { gasPricesStartPolling, @@ -24,25 +23,24 @@ import { gasUpdateTxFee, } from '@/redux/gas'; import { ethereumUtils } from '@/utils'; -import { getNetworkObj } from '@/networks'; +import { getNetworkObject } from '@/networks'; import { useExternalToken } from '@/resources/assets/externalAssetsQuery'; import { BNB_MAINNET_ADDRESS, ETH_ADDRESS, MATIC_MAINNET_ADDRESS } from '@/references'; import useAccountSettings from './useAccountSettings'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; -const checkSufficientGas = (txFee: LegacyGasFee | GasFee, network: Network, nativeAsset?: ParsedAddressAsset) => { - const isLegacyGasNetwork = getNetworkObj(network).gas.gasType === 'legacy'; +const checkSufficientGas = (txFee: LegacyGasFee | GasFee, chainId: ChainId, nativeAsset?: ParsedAddressAsset) => { + const isLegacyGasNetwork = getNetworkObject({ chainId }).gas.gasType === 'legacy'; const txFeeValue = isLegacyGasNetwork ? (txFee as LegacyGasFee)?.estimatedFee : (txFee as GasFee)?.maxFee; - const chainId = ethereumUtils.getChainIdFromNetwork(network); - const networkNativeAsset = nativeAsset || ethereumUtils.getNetworkNativeAsset(chainId); + const networkNativeAsset = nativeAsset || ethereumUtils.getNetworkNativeAsset({ chainId }); const balanceAmount = networkNativeAsset?.balance?.amount || 0; const txFeeAmount = fromWei(txFeeValue?.value?.amount); const isSufficientGas = greaterThanOrEqualTo(balanceAmount, txFeeAmount); return isSufficientGas; }; -const checkValidGas = (selectedGasParams: LegacyGasFeeParams | GasFeeParams, network: Network) => { - const isLegacyGasNetwork = getNetworkObj(network).gas.gasType === 'legacy'; +const checkValidGas = (selectedGasParams: LegacyGasFeeParams | GasFeeParams, chainId: ChainId) => { + const isLegacyGasNetwork = getNetworkObject({ chainId }).gas.gasType === 'legacy'; const gasValue = isLegacyGasNetwork ? (selectedGasParams as LegacyGasFeeParams)?.gasPrice : (selectedGasParams as GasFeeParams)?.maxBaseFee; @@ -50,8 +48,8 @@ const checkValidGas = (selectedGasParams: LegacyGasFeeParams | GasFeeParams, net return isValidGas; }; -const checkGasReady = (txFee: LegacyGasFee | GasFee, selectedGasParams: LegacyGasFeeParams | GasFeeParams, network: Network) => { - const isLegacyGasNetwork = getNetworkObj(network).gas.gasType === 'legacy'; +const checkGasReady = (txFee: LegacyGasFee | GasFee, selectedGasParams: LegacyGasFeeParams | GasFeeParams, chainId: ChainId) => { + const isLegacyGasNetwork = getNetworkObject({ chainId }).gas.gasType === 'legacy'; const gasValue = isLegacyGasNetwork ? (selectedGasParams as LegacyGasFeeParams)?.gasPrice : (selectedGasParams as GasFeeParams)?.maxBaseFee; @@ -88,7 +86,7 @@ export default function useGas({ nativeAsset }: { nativeAsset?: ParsedAddressAss gasLimit: string; selectedGasFee: SelectedGasFee; selectedGasFeeOption: string; - txNetwork: Network; + chainId: ChainId; l1GasFeeOptimism: string; } = useSelector( ({ @@ -100,7 +98,7 @@ export default function useGas({ nativeAsset }: { nativeAsset?: ParsedAddressAss gasLimit, l1GasFeeOptimism, selectedGasFee, - txNetwork, + chainId, }, }: AppState) => ({ currentBlockParams, @@ -111,29 +109,29 @@ export default function useGas({ nativeAsset }: { nativeAsset?: ParsedAddressAss l1GasFeeOptimism, selectedGasFee, selectedGasFeeOption: selectedGasFee.option, - txNetwork, + chainId, }) ); const prevSelectedGasFee = usePrevious(gasData?.selectedGasFee); const isSufficientGas = useMemo( - () => checkSufficientGas(gasData?.selectedGasFee?.gasFee, gasData?.txNetwork, nativeAsset), - [gasData?.selectedGasFee?.gasFee, gasData?.txNetwork, nativeAsset] + () => checkSufficientGas(gasData?.selectedGasFee?.gasFee, gasData?.chainId, nativeAsset), + [gasData?.selectedGasFee?.gasFee, gasData?.chainId, nativeAsset] ); const isValidGas = useMemo( - () => checkValidGas(gasData?.selectedGasFee?.gasFeeParams, gasData?.txNetwork), - [gasData?.selectedGasFee, gasData?.txNetwork] + () => checkValidGas(gasData?.selectedGasFee?.gasFeeParams, gasData?.chainId), + [gasData?.selectedGasFee, gasData?.chainId] ); const isGasReady = useMemo( - () => checkGasReady(gasData?.selectedGasFee?.gasFee, gasData?.selectedGasFee?.gasFeeParams, gasData?.txNetwork), - [gasData?.selectedGasFee?.gasFee, gasData?.selectedGasFee?.gasFeeParams, gasData?.txNetwork] + () => checkGasReady(gasData?.selectedGasFee?.gasFee, gasData?.selectedGasFee?.gasFeeParams, gasData?.chainId), + [gasData?.selectedGasFee?.gasFee, gasData?.selectedGasFee?.gasFeeParams, gasData?.chainId] ); const startPollingGasFees = useCallback( - (network = networkTypes.mainnet, flashbots = false) => dispatch(gasPricesStartPolling(network, flashbots)), + (chainId = ChainId.mainnet, flashbots = false) => dispatch(gasPricesStartPolling(chainId, flashbots)), [dispatch] ); const stopPollingGasFees = useCallback(() => dispatch(gasPricesStopPolling()), [dispatch]); @@ -153,12 +151,12 @@ export default function useGas({ nativeAsset }: { nativeAsset?: ParsedAddressAss const getTotalGasPrice = useCallback(() => { const txFee = gasData?.selectedGasFee?.gasFee; - const isLegacyGasNetwork = getNetworkObj(gasData?.txNetwork).gas.gasType === 'legacy'; + const isLegacyGasNetwork = getNetworkObject({ chainId: gasData?.chainId }).gas.gasType === 'legacy'; const txFeeValue = isLegacyGasNetwork ? (txFee as LegacyGasFee)?.estimatedFee : (txFee as GasFee)?.maxFee; const txFeeAmount = fromWei(txFeeValue?.value?.amount); return txFeeAmount; - }, [gasData?.selectedGasFee?.gasFee, gasData?.txNetwork]); + }, [gasData?.selectedGasFee?.gasFee, gasData?.chainId]); return { isGasReady, diff --git a/src/hooks/useHasEnoughBalance.ts b/src/hooks/useHasEnoughBalance.ts new file mode 100644 index 00000000000..4217091802c --- /dev/null +++ b/src/hooks/useHasEnoughBalance.ts @@ -0,0 +1,47 @@ +import { useEffect, useState } from 'react'; +import { fromWei, greaterThanOrEqualTo } from '@/helpers/utilities'; +import BigNumber from 'bignumber.js'; +import { SelectedGasFee } from '@/entities'; +import { ChainId } from '@/networks/types'; + +type WalletBalance = { + amount: string | number; + display: string; + isLoaded: boolean; + symbol: string; +}; + +type BalanceCheckParams = { + isMessageRequest: boolean; + walletBalance: WalletBalance; + chainId: ChainId; + selectedGasFee: SelectedGasFee; + req: any; +}; + +export const useHasEnoughBalance = ({ isMessageRequest, walletBalance, chainId, selectedGasFee, req }: BalanceCheckParams) => { + const [isBalanceEnough, setIsBalanceEnough] = useState(); + + useEffect(() => { + if (isMessageRequest) { + setIsBalanceEnough(true); + return; + } + + const { gasFee } = selectedGasFee; + if (!walletBalance.isLoaded || !chainId || !gasFee?.estimatedFee) { + return; + } + + const txFeeAmount = fromWei(gasFee?.maxFee?.value?.amount ?? 0); + const balanceAmount = walletBalance.amount; + const value = req?.value ?? 0; + + const totalAmount = new BigNumber(fromWei(value)).plus(txFeeAmount); + const isEnough = greaterThanOrEqualTo(balanceAmount, totalAmount); + + setIsBalanceEnough(isEnough); + }, [isMessageRequest, chainId, selectedGasFee, walletBalance, req]); + + return { isBalanceEnough }; +}; diff --git a/src/hooks/useImportingWallet.ts b/src/hooks/useImportingWallet.ts index 828dee3225c..1523bae55c4 100644 --- a/src/hooks/useImportingWallet.ts +++ b/src/hooks/useImportingWallet.ts @@ -17,7 +17,7 @@ import { WrappedAlert as Alert } from '@/helpers/alert'; import { analytics } from '@/analytics'; import { PROFILES, useExperimentalFlag } from '@/config'; import { fetchReverseRecord } from '@/handlers/ens'; -import { getProviderForNetwork, isValidBluetoothDeviceId, resolveUnstoppableDomain } from '@/handlers/web3'; +import { getProvider, isValidBluetoothDeviceId, resolveUnstoppableDomain } from '@/handlers/web3'; import { isENSAddressFormat, isUnstoppableAddressFormat, isValidWallet } from '@/helpers/validators'; import WalletBackupStepTypes from '@/helpers/walletBackupStepTypes'; import { walletInit } from '@/model/wallet'; @@ -31,6 +31,7 @@ import { handleReviewPromptAction } from '@/utils/reviewAlert'; import { ReviewPromptAction } from '@/storage/schema'; import { checkWalletsForBackupStatus } from '@/screens/SettingsSheet/utils'; import walletBackupTypes from '@/helpers/walletBackupTypes'; +import { ChainId } from '@/networks/types'; export default function useImportingWallet({ showImportModal = true } = {}) { const { accountAddress } = useAccountSettings(); @@ -122,9 +123,9 @@ export default function useImportingWallet({ showImportModal = true } = {}) { // Validate ENS if (isENSAddressFormat(input)) { try { - const web3Provider = getProviderForNetwork(); + const provider = getProvider({ chainId: ChainId.mainnet }); const [address, avatar] = await Promise.all([ - web3Provider.resolveName(input), + provider.resolveName(input), !avatarUrl && profilesEnabled && fetchENSAvatar(input, { swallowError: true }), ]); if (!address) { diff --git a/src/hooks/useNonceForDisplay.ts b/src/hooks/useNonceForDisplay.ts new file mode 100644 index 00000000000..ca7b302bedb --- /dev/null +++ b/src/hooks/useNonceForDisplay.ts @@ -0,0 +1,32 @@ +import { useEffect, useState } from 'react'; +import { getNextNonce } from '@/state/nonces'; +import { ChainId } from '@/networks/types'; +import { logger, RainbowError } from '@/logger'; + +type UseNonceParams = { + isMessageRequest: boolean; + address: string; + chainId: ChainId; +}; + +export const useNonceForDisplay = ({ isMessageRequest, address, chainId }: UseNonceParams) => { + const [nonceForDisplay, setNonceForDisplay] = useState(); + + useEffect(() => { + if (!isMessageRequest && !nonceForDisplay) { + (async () => { + try { + const nonce = await getNextNonce({ address, chainId }); + if (nonce || nonce === 0) { + const nonceAsString = nonce.toString(); + setNonceForDisplay(nonceAsString); + } + } catch (error) { + logger.error(new RainbowError(`[useNonceForDisplay]: Failed to get nonce for display: ${error}`)); + } + })(); + } + }, [address, chainId, isMessageRequest, nonceForDisplay]); + + return { nonceForDisplay }; +}; diff --git a/src/hooks/useOnAvatarPress.ts b/src/hooks/useOnAvatarPress.ts index 4c7323bcf24..bda2d6f20f5 100644 --- a/src/hooks/useOnAvatarPress.ts +++ b/src/hooks/useOnAvatarPress.ts @@ -1,5 +1,5 @@ import lang from 'i18n-js'; -import { useCallback, useEffect, useMemo } from 'react'; +import { useCallback } from 'react'; import { Linking } from 'react-native'; import { ImageOrVideo } from 'react-native-image-crop-picker'; import { useDispatch } from 'react-redux'; @@ -139,7 +139,7 @@ export default ({ screenType = 'transaction' }: UseOnAvatarPressProps = {}) => { const isReadOnly = isReadOnlyWallet && !enableActionsOnReadOnlyWallet; const isENSProfile = profilesEnabled && profileEnabled && isOwner; - const isZeroETH = isZero(accountAsset?.balance?.amount); + const isZeroETH = isZero(accountAsset?.balance?.amount || 0); const callback = useCallback( async (buttonIndex: number) => { diff --git a/src/hooks/usePriceImpactDetails.ts b/src/hooks/usePriceImpactDetails.ts index f6789c38b76..b077d816a9b 100644 --- a/src/hooks/usePriceImpactDetails.ts +++ b/src/hooks/usePriceImpactDetails.ts @@ -1,7 +1,6 @@ import { useMemo } from 'react'; import useAccountSettings from './useAccountSettings'; import { SwappableAsset } from '@/entities'; -import { Network } from '@/helpers'; import { useTheme } from '@/theme'; import { @@ -14,9 +13,9 @@ import { } from '@/helpers/utilities'; import { CrosschainQuote, Quote } from '@rainbow-me/swaps'; -import ethereumUtils, { useNativeAsset } from '@/utils/ethereumUtils'; +import { useNativeAsset } from '@/utils/ethereumUtils'; import { isUnwrapNative, isWrapNative } from '@/handlers/swap'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; export enum SwapPriceImpactType { none = 'none', diff --git a/src/hooks/useProviderSetup.ts b/src/hooks/useProviderSetup.ts new file mode 100644 index 00000000000..bd1daa5b854 --- /dev/null +++ b/src/hooks/useProviderSetup.ts @@ -0,0 +1,45 @@ +import { useEffect, useState } from 'react'; +import { getFlashbotsProvider, getProvider } from '@/handlers/web3'; +import { StaticJsonRpcProvider } from '@ethersproject/providers'; +import { ethereumUtils } from '@/utils'; +import { getOnchainAssetBalance } from '@/handlers/assets'; +import { ParsedAddressAsset } from '@/entities'; +import { ChainId } from '@/networks/types'; + +export const useProviderSetup = (chainId: ChainId, address: string) => { + const [provider, setProvider] = useState(null); + const [nativeAsset, setNativeAsset] = useState(null); + + useEffect(() => { + const initProvider = async () => { + let p; + if (chainId === ChainId.mainnet) { + p = await getFlashbotsProvider(); + } else { + p = getProvider({ chainId }); + } + setProvider(p); + }; + initProvider(); + }, [chainId]); + + useEffect(() => { + const fetchNativeAsset = async () => { + if (provider) { + const asset = await ethereumUtils.getNativeAssetForNetwork({ chainId, address }); + if (asset) { + const balance = await getOnchainAssetBalance(asset, address, chainId, provider); + if (balance) { + const assetWithOnchainBalance: ParsedAddressAsset = { ...asset, balance }; + setNativeAsset(assetWithOnchainBalance); + } else { + setNativeAsset(asset); + } + } + } + }; + fetchNativeAsset(); + }, [address, chainId, provider]); + + return { provider, nativeAsset }; +}; diff --git a/src/hooks/useRainbowFee.js b/src/hooks/useRainbowFee.js index b80d489a6be..6cc0d416bef 100644 --- a/src/hooks/useRainbowFee.js +++ b/src/hooks/useRainbowFee.js @@ -47,7 +47,7 @@ export default function useRainbowFee({ tradeDetails, chainId }) { useEffect(() => { const getNativeAsset = async () => { - const nativeAsset = await ethereumUtils.getNativeAssetForNetwork(chainId, accountAddress); + const nativeAsset = await ethereumUtils.getNativeAssetForNetwork({ chainId, address: accountAddress }); setNativeAsset(nativeAsset); }; !nativeAsset && getNativeAsset(); diff --git a/src/hooks/useRefreshAccountData.ts b/src/hooks/useRefreshAccountData.ts index 056b3c1af94..f9cb7ab786c 100644 --- a/src/hooks/useRefreshAccountData.ts +++ b/src/hooks/useRefreshAccountData.ts @@ -1,7 +1,6 @@ import delay from 'delay'; import { useCallback, useMemo, useState } from 'react'; import { useDispatch } from 'react-redux'; -import { getIsHardhatConnected } from '@/handlers/web3'; import { walletConnectLoadState } from '../redux/walletconnect'; import { fetchWalletENSAvatars, fetchWalletNames } from '../redux/wallets'; import useAccountSettings from './useAccountSettings'; @@ -16,6 +15,7 @@ import useNftSort from './useNFTsSortBy'; import { Address } from 'viem'; import { addysSummaryQueryKey } from '@/resources/summary/summary'; import useWallets from './useWallets'; +import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; export default function useRefreshAccountData() { const dispatch = useDispatch(); @@ -23,6 +23,7 @@ export default function useRefreshAccountData() { const [isRefreshing, setIsRefreshing] = useState(false); const profilesEnabled = useExperimentalFlag(PROFILES); const { nftSort } = useNftSort(); + const { connectedToHardhat } = useConnectedToHardhatStore(); const { wallets } = useWallets(); @@ -32,8 +33,6 @@ export default function useRefreshAccountData() { ); const fetchAccountData = useCallback(async () => { - const connectedToHardhat = getIsHardhatConnected(); - queryClient.invalidateQueries(nftsQueryKey({ address: accountAddress, sortBy: nftSort })); queryClient.invalidateQueries(positionsQueryKey({ address: accountAddress as Address, currency: nativeCurrency })); queryClient.invalidateQueries(addysSummaryQueryKey({ addresses: allAddresses, currency: nativeCurrency })); @@ -56,7 +55,7 @@ export default function useRefreshAccountData() { logger.error(new RainbowError(`[useRefreshAccountData]: Error refreshing data: ${error}`)); throw error; } - }, [accountAddress, dispatch, nativeCurrency, profilesEnabled]); + }, [accountAddress, allAddresses, connectedToHardhat, dispatch, nativeCurrency, nftSort, profilesEnabled]); const refresh = useCallback(async () => { if (isRefreshing) return; diff --git a/src/hooks/useSearchCurrencyList.ts b/src/hooks/useSearchCurrencyList.ts index 59b1320d29b..16c8bf2149d 100644 --- a/src/hooks/useSearchCurrencyList.ts +++ b/src/hooks/useSearchCurrencyList.ts @@ -18,7 +18,7 @@ import { CROSSCHAIN_SWAPS, useExperimentalFlag } from '@/config'; import { IS_TEST } from '@/env'; import { useFavorites } from '@/resources/favorites'; import { getUniqueId } from '@/utils/ethereumUtils'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; type swapCurrencyListType = | 'verifiedAssets' @@ -86,7 +86,6 @@ const useSearchCurrencyList = (searchQuery: string, searchChainId = ChainId.main const [highLiquidityAssets, setHighLiquidityAssets] = useState([]); const [lowLiquidityAssets, setLowLiquidityAssets] = useState([]); const [verifiedAssets, setVerifiedAssets] = useState([]); - const [fetchingCrosschainAssets, setFetchingCrosschainAssets] = useState(false); const [crosschainVerifiedAssets, setCrosschainVerifiedAssets] = useState({ [ChainId.mainnet]: [], @@ -387,7 +386,7 @@ const useSearchCurrencyList = (searchQuery: string, searchChainId = ChainId.main if (inputCurrency?.name && verifiedAssets.length) { if (bridgeAsset) { list.push({ - color: colors.networkColors[bridgeAsset.network], + color: colors.networkColors[bridgeAsset.chainId], data: [bridgeAsset], key: 'bridgeAsset', title: lang.t(`exchange.token_sections.${tokenSectionTypes.bridgeTokenSection}`), @@ -430,7 +429,7 @@ const useSearchCurrencyList = (searchQuery: string, searchChainId = ChainId.main bridgeAsset = curatedAssets.find(asset => asset?.name === inputCurrency?.name); if (bridgeAsset) { list.push({ - color: colors.networkColors[bridgeAsset.network], + color: colors.networkColors[bridgeAsset.chainId], data: [bridgeAsset], key: 'bridgeAsset', title: lang.t(`exchange.token_sections.${tokenSectionTypes.bridgeTokenSection}`), diff --git a/src/hooks/useSubmitTransaction.ts b/src/hooks/useSubmitTransaction.ts new file mode 100644 index 00000000000..8d6f861ae25 --- /dev/null +++ b/src/hooks/useSubmitTransaction.ts @@ -0,0 +1,56 @@ +import { useCallback, useState } from 'react'; +import { performanceTracking, TimeToSignOperation } from '@/state/performance/performance'; +import Routes from '@/navigation/routesNames'; +import { useNavigation } from '@/navigation'; +import { RequestSource } from '@/utils/requestNavigationHandlers'; +import { SCREEN_FOR_REQUEST_SOURCE } from '@/components/Transactions/constants'; +import { logger, RainbowError } from '@/logger'; + +export const useTransactionSubmission = ({ + isBalanceEnough, + accountInfo, + onConfirm, + source, +}: { + isBalanceEnough: boolean | undefined; + accountInfo: { isHardwareWallet: boolean }; + onConfirm: () => Promise; + source: RequestSource; +}) => { + const [isAuthorizing, setIsAuthorizing] = useState(false); + const { navigate } = useNavigation(); + + const onPressSend = useCallback(async () => { + if (isAuthorizing) return; + try { + setIsAuthorizing(true); + await onConfirm(); + } catch (error) { + logger.error(new RainbowError(`[useTransactionSubmission]: Error while sending transaction: ${error}`)); + } finally { + setIsAuthorizing(false); + } + }, [isAuthorizing, onConfirm]); + + const submitFn = useCallback( + () => + performanceTracking.getState().executeFn({ + fn: async () => { + if (!isBalanceEnough) { + navigate(Routes.ADD_CASH_SHEET); + return; + } + if (accountInfo.isHardwareWallet) { + navigate(Routes.HARDWARE_WALLET_TX_NAVIGATOR, { submit: onPressSend }); + } else { + await onPressSend(); + } + }, + operation: TimeToSignOperation.CallToAction, + screen: SCREEN_FOR_REQUEST_SOURCE[source], + })(), + [accountInfo.isHardwareWallet, isBalanceEnough, navigate, onPressSend, source] + ); + + return { submitFn, isAuthorizing }; +}; diff --git a/src/hooks/useSwapCurrencyHandlers.ts b/src/hooks/useSwapCurrencyHandlers.ts index 3fce074a3cb..9b835ff7a2d 100644 --- a/src/hooks/useSwapCurrencyHandlers.ts +++ b/src/hooks/useSwapCurrencyHandlers.ts @@ -119,8 +119,6 @@ export default function useSwapCurrencyHandlers({ } : null; - // prefetchExternalToken({address: newInputCurrency.address, network: newInputCurrency.network, currency: nativeCurrency}) - dispatch(updateSwapInputCurrency(newInputCurrency, crosschainSwapsEnabled)); setLastFocusedInputHandle?.(inputFieldRef); handleNavigate?.(newInputCurrency); @@ -136,7 +134,6 @@ export default function useSwapCurrencyHandlers({ } : null; - // prefetchExternalToken({address: newOutputCurrency.address, network: newOutputCurrency.network, currency: nativeCurrency}) dispatch(updateSwapOutputCurrency(newOutputCurrency, crosschainSwapsEnabled)); setLastFocusedInputHandle?.(inputFieldRef); handleNavigate?.(newOutputCurrency); diff --git a/src/hooks/useSwapCurrencyList.ts b/src/hooks/useSwapCurrencyList.ts index 12b8cf47df0..02bd432f880 100644 --- a/src/hooks/useSwapCurrencyList.ts +++ b/src/hooks/useSwapCurrencyList.ts @@ -6,22 +6,20 @@ import { rankings } from 'match-sorter'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useTheme } from '../theme/ThemeContext'; import usePrevious from './usePrevious'; -import { RainbowToken, RainbowToken as RT, TokenSearchTokenListId } from '@/entities'; +import { RainbowToken, TokenSearchTokenListId } from '@/entities'; import { swapSearch } from '@/handlers/tokenSearch'; -import { addHexPrefix, getProviderForNetwork } from '@/handlers/web3'; +import { addHexPrefix, getProvider } from '@/handlers/web3'; import tokenSectionTypes from '@/helpers/tokenSectionTypes'; import { DAI_ADDRESS, erc20ABI, ETH_ADDRESS, rainbowTokenList, USDC_ADDRESS, WBTC_ADDRESS, WETH_ADDRESS } from '@/references'; import { ethereumUtils, filterList, isLowerCaseMatch } from '@/utils'; import useSwapCurrencies from '@/hooks/useSwapCurrencies'; -import { Network } from '@/helpers'; import { CROSSCHAIN_SWAPS, useExperimentalFlag } from '@/config'; import { IS_TEST } from '@/env'; import { useFavorites } from '@/resources/favorites'; import { getUniqueId } from '@/utils/ethereumUtils'; -import { ChainId } from '@/__swaps__/types/chains'; import { logger } from '@/logger'; +import { ChainId, Network } from '@/networks/types'; -const MAINNET_CHAINID = 1; type swapCurrencyListType = | 'verifiedAssets' | 'highLiquidityAssets' @@ -30,13 +28,10 @@ type swapCurrencyListType = | 'curatedAssets' | 'importedAssets'; -type CrosschainVerifiedAssets = { - [Network.mainnet]: RT[]; - [Network.optimism]: RT[]; - [Network.polygon]: RT[]; - [Network.bsc]: RT[]; - [Network.arbitrum]: RT[]; -}; +type CrosschainVerifiedAssets = Record< + ChainId.mainnet | ChainId.optimism | ChainId.polygon | ChainId.bsc | ChainId.arbitrum, + RainbowToken[] +>; const abcSort = (list: any[], key?: string) => { return list.sort((a, b) => { @@ -47,16 +42,16 @@ const abcSort = (list: any[], key?: string) => { const searchCurrencyList = async (searchParams: { chainId: number; fromChainId?: number | ''; - searchList: RT[] | TokenSearchTokenListId; + searchList: RainbowToken[] | TokenSearchTokenListId; query: string; }) => { const { searchList, query, chainId, fromChainId } = searchParams; const isAddress = query.match(/^(0x)?[0-9a-fA-F]{40}$/); - const keys: (keyof RT)[] = isAddress ? ['address'] : ['symbol', 'name']; + const keys: (keyof RainbowToken)[] = isAddress ? ['address'] : ['symbol', 'name']; const formattedQuery = isAddress ? addHexPrefix(query).toLowerCase() : query; if (typeof searchList === 'string') { const threshold = isAddress ? 'CASE_SENSITIVE_EQUAL' : 'CONTAINS'; - if (chainId === MAINNET_CHAINID && !formattedQuery && searchList !== 'verifiedAssets') { + if (chainId === ChainId.mainnet && !formattedQuery && searchList !== 'verifiedAssets') { return []; } return swapSearch({ @@ -74,10 +69,10 @@ const searchCurrencyList = async (searchParams: { } }; -const useSwapCurrencyList = (searchQuery: string, searchChainId = MAINNET_CHAINID, isDiscover = false) => { +const useSwapCurrencyList = (searchQuery: string, searchChainId = ChainId.mainnet, isDiscover = false) => { const previousChainId = usePrevious(searchChainId); - const searching = useMemo(() => searchQuery !== '' || MAINNET_CHAINID !== searchChainId, [searchChainId, searchQuery]); + const searching = useMemo(() => searchQuery !== '' || ChainId.mainnet !== searchChainId, [searchChainId, searchQuery]); const { favorites: favoriteAddresses, favoritesMetadata: favoriteMap } = useFavorites(); @@ -85,24 +80,24 @@ const useSwapCurrencyList = (searchQuery: string, searchChainId = MAINNET_CHAINI const unfilteredFavorites = Object.values(favoriteMap); const [loading, setLoading] = useState(true); - const [favoriteAssets, setFavoriteAssets] = useState([]); - const [importedAssets, setImportedAssets] = useState([]); - const [highLiquidityAssets, setHighLiquidityAssets] = useState([]); - const [lowLiquidityAssets, setLowLiquidityAssets] = useState([]); - const [verifiedAssets, setVerifiedAssets] = useState([]); + const [favoriteAssets, setFavoriteAssets] = useState([]); + const [importedAssets, setImportedAssets] = useState([]); + const [highLiquidityAssets, setHighLiquidityAssets] = useState([]); + const [lowLiquidityAssets, setLowLiquidityAssets] = useState([]); + const [verifiedAssets, setVerifiedAssets] = useState([]); const [fetchingCrosschainAssets, setFetchingCrosschainAssets] = useState(false); const [crosschainVerifiedAssets, setCrosschainVerifiedAssets] = useState({ - [Network.mainnet]: [], - [Network.optimism]: [], - [Network.polygon]: [], - [Network.bsc]: [], - [Network.arbitrum]: [], + [ChainId.mainnet]: [], + [ChainId.optimism]: [], + [ChainId.polygon]: [], + [ChainId.bsc]: [], + [ChainId.arbitrum]: [], }); const crosschainSwapsEnabled = useExperimentalFlag(CROSSCHAIN_SWAPS); const { inputCurrency } = useSwapCurrencies(); - const previousInputCurrencyNetwork = usePrevious(inputCurrency?.network); - const inputChainId = useMemo(() => ethereumUtils.getChainIdFromNetwork(inputCurrency?.network), [inputCurrency?.network]); + const previousInputCurrencyChainId = usePrevious(inputCurrency?.chainId); + const inputChainId = inputCurrency?.chainId; const isCrosschainSearch = useMemo(() => { if (inputChainId && inputChainId !== searchChainId && crosschainSwapsEnabled && !isDiscover) { return true; @@ -114,17 +109,16 @@ const useSwapCurrencyList = (searchQuery: string, searchChainId = MAINNET_CHAINI [favoriteAddresses] ); const handleSearchResponse = useCallback( - (tokens: RT[], crosschainNetwork?: Network) => { + (tokens: RainbowToken[], chainId?: ChainId) => { // These transformations are necessary for L2 tokens to match our spec - const activeChainId = crosschainNetwork ? ethereumUtils.getChainIdFromNetwork(crosschainNetwork) : searchChainId; + const activeChainId = chainId ? chainId : searchChainId; return (tokens || []) .map(token => { token.address = token.networks?.[activeChainId]?.address || token.address; - const network = crosschainNetwork || ethereumUtils.getNetworkFromChainId(searchChainId); - token.network = network; - if (token.networks[MAINNET_CHAINID]) { - token.mainnet_address = token.networks[MAINNET_CHAINID].address; + token.chainId = activeChainId; + if (token.networks[ChainId.mainnet]) { + token.mainnet_address = token.networks[ChainId.mainnet].address; } token.uniqueId = getUniqueId(token.address, activeChainId); @@ -159,6 +153,7 @@ const useSwapCurrencyList = (searchQuery: string, searchChainId = MAINNET_CHAINI return { ...token, network: Network.mainnet, + chainId: ChainId.mainnet, uniqueId: getUniqueId(token.address, ChainId.mainnet), }; }); @@ -175,15 +170,14 @@ const useSwapCurrencyList = (searchQuery: string, searchChainId = MAINNET_CHAINI }, [searchChainId, searchQuery, searching, unfilteredFavorites]); const getImportedAsset = useCallback( - async (searchQuery: string, chainId: number): Promise => { + async (searchQuery: string, chainId: number): Promise => { if (searching) { if (isAddress(searchQuery)) { const tokenListEntry = rainbowTokenList.RAINBOW_TOKEN_LIST[searchQuery.toLowerCase()]; if (tokenListEntry) { return [tokenListEntry]; } - const network = ethereumUtils.getNetworkFromChainId(chainId); - const provider = getProviderForNetwork(network); + const provider = getProvider({ chainId }); const tokenContract = new Contract(searchQuery, erc20ABI, provider); try { const [name, symbol, decimals, address] = await Promise.all([ @@ -192,10 +186,11 @@ const useSwapCurrencyList = (searchQuery: string, searchChainId = MAINNET_CHAINI tokenContract.decimals(), getAddress(searchQuery), ]); - const uniqueId = `${address}_${network}`; + const uniqueId = getUniqueId(address, chainId); return [ { address, + chainId, decimals, favorite: false, highLiquidity: false, @@ -209,7 +204,7 @@ const useSwapCurrencyList = (searchQuery: string, searchChainId = MAINNET_CHAINI }, }, symbol, - network, + network: ethereumUtils.getNetworkFromChainId(chainId), uniqueId, } as RainbowToken, ]; @@ -225,18 +220,17 @@ const useSwapCurrencyList = (searchQuery: string, searchChainId = MAINNET_CHAINI ); const getCrosschainVerifiedAssetsForNetwork = useCallback( - async (network: Network) => { - const crosschainId = ethereumUtils.getChainIdFromNetwork(network); - const fromChainId = inputChainId !== crosschainId ? inputChainId : ''; + async (chainId: ChainId) => { + const fromChainId = inputChainId !== chainId ? inputChainId : ''; const results = await searchCurrencyList({ searchList: 'verifiedAssets', query: '', - chainId: crosschainId, + chainId, fromChainId, }); setCrosschainVerifiedAssets(state => ({ ...state, - [network]: handleSearchResponse(results, network), + [chainId]: handleSearchResponse(results, chainId), })); }, [handleSearchResponse, inputChainId] @@ -244,8 +238,8 @@ const useSwapCurrencyList = (searchQuery: string, searchChainId = MAINNET_CHAINI const getCrosschainVerifiedAssets = useCallback(async () => { const crosschainAssetRequests: Promise[] = []; - Object.keys(crosschainVerifiedAssets).forEach(network => { - crosschainAssetRequests.push(getCrosschainVerifiedAssetsForNetwork(network as Network)); + Object.keys(crosschainVerifiedAssets).forEach(chainIdKey => { + crosschainAssetRequests.push(getCrosschainVerifiedAssetsForNetwork(Number(chainIdKey))); }); await Promise.all(crosschainAssetRequests); }, [crosschainVerifiedAssets, getCrosschainVerifiedAssetsForNetwork]); @@ -306,7 +300,7 @@ const useSwapCurrencyList = (searchQuery: string, searchChainId = MAINNET_CHAINI const search = useCallback(async () => { const categories: swapCurrencyListType[] = - searchChainId === MAINNET_CHAINID + searchChainId === ChainId.mainnet ? ['favoriteAssets', 'highLiquidityAssets', 'verifiedAssets', 'importedAssets'] : ['verifiedAssets', 'importedAssets']; setLoading(true); @@ -351,9 +345,9 @@ const useSwapCurrencyList = (searchQuery: string, searchChainId = MAINNET_CHAINI (searching && !wasSearching) || (searching && previousSearchQuery !== searchQuery) || searchChainId !== previousChainId || - inputCurrency?.network !== previousInputCurrencyNetwork + inputCurrency?.chainId !== previousInputCurrencyChainId ) { - if (searchChainId === MAINNET_CHAINID) { + if (searchChainId === ChainId.mainnet) { search(); slowSearch(); } else { @@ -368,14 +362,14 @@ const useSwapCurrencyList = (searchQuery: string, searchChainId = MAINNET_CHAINI }; doSearch(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [searching, searchQuery, searchChainId, isCrosschainSearch, inputCurrency?.network]); + }, [searching, searchQuery, searchChainId, isCrosschainSearch, inputCurrency?.chainId]); const { colors } = useTheme(); const currencyList = useMemo(() => { const list = []; let bridgeAsset = isCrosschainSearch - ? verifiedAssets.find(asset => isLowerCaseMatch(asset?.name, inputCurrency?.name) && asset?.network !== inputCurrency?.network) + ? verifiedAssets.find(asset => isLowerCaseMatch(asset?.name, inputCurrency?.name) && asset?.chainId !== inputCurrency?.chainId) : null; if (searching) { const importedAsset = importedAssets?.[0]; @@ -400,14 +394,14 @@ const useSwapCurrencyList = (searchQuery: string, searchChainId = MAINNET_CHAINI if (inputCurrency?.name && verifiedAssets.length) { if (bridgeAsset) { list.push({ - color: colors.networkColors[bridgeAsset.network], + color: colors.networkColors[bridgeAsset.chainId], data: [bridgeAsset], key: 'bridgeAsset', title: lang.t(`exchange.token_sections.${tokenSectionTypes.bridgeTokenSection}`), }); } } - if (favoriteAssets?.length && searchChainId === MAINNET_CHAINID) { + if (favoriteAssets?.length && searchChainId === ChainId.mainnet) { list.push({ color: colors.yellowFavorite, data: abcSort(favoriteAssets, 'name'), @@ -438,12 +432,12 @@ const useSwapCurrencyList = (searchQuery: string, searchChainId = MAINNET_CHAINI }); } } else { - const curatedAssets = searchChainId === MAINNET_CHAINID && getCurated(); + const curatedAssets = searchChainId === ChainId.mainnet && getCurated(); if (inputCurrency?.name && isCrosschainSearch && curatedAssets) { bridgeAsset = curatedAssets.find(asset => asset?.name === inputCurrency?.name); if (bridgeAsset) { list.push({ - color: colors.networkColors[bridgeAsset.network], + color: colors.networkColors[bridgeAsset.chainId], data: [bridgeAsset], key: 'bridgeAsset', title: lang.t(`exchange.token_sections.${tokenSectionTypes.bridgeTokenSection}`), @@ -469,39 +463,39 @@ const useSwapCurrencyList = (searchQuery: string, searchChainId = MAINNET_CHAINI } return list; }, [ + isCrosschainSearch, + verifiedAssets, searching, + inputCurrency?.name, + inputCurrency?.chainId, importedAssets, - favoriteAssets, - verifiedAssets, highLiquidityAssets, lowLiquidityAssets, - colors.yellowFavorite, - unfilteredFavorites, - searchChainId, - getCurated, isFavorite, - inputCurrency?.name, + favoriteAssets, + searchChainId, colors.networkColors, - isCrosschainSearch, - inputCurrency?.network, + colors.yellowFavorite, + getCurated, + unfilteredFavorites, ]); const crosschainExactMatches = useMemo(() => { if (currencyList.length) return []; if (!searchQuery) return []; - const exactMatches: RT[] = []; - Object.keys(crosschainVerifiedAssets).forEach(network => { - const currentNetworkChainId = ethereumUtils.getChainIdFromNetwork(network as Network); - if (currentNetworkChainId !== searchChainId) { + const exactMatches: RainbowToken[] = []; + Object.keys(crosschainVerifiedAssets).forEach(chainIdKey => { + const chainId = Number(chainIdKey); + if (chainId !== searchChainId) { // including goerli in our networks type is causing this type issue // @ts-ignore - const exactMatch = crosschainVerifiedAssets[network as Network].find((asset: RT) => { + const exactMatch = crosschainVerifiedAssets[chainId].find((asset: RainbowToken) => { const symbolMatch = isLowerCaseMatch(asset?.symbol, searchQuery); const nameMatch = isLowerCaseMatch(asset?.name, searchQuery); return symbolMatch || nameMatch; }); if (exactMatch) { - exactMatches.push({ ...exactMatch, network }); + exactMatches.push({ ...exactMatch, chainId }); } } }); diff --git a/src/hooks/useSwapDerivedOutputs.ts b/src/hooks/useSwapDerivedOutputs.ts index 72c76856c54..dfcd744ea3c 100644 --- a/src/hooks/useSwapDerivedOutputs.ts +++ b/src/hooks/useSwapDerivedOutputs.ts @@ -65,13 +65,13 @@ const getInputAmount = async ( if (!inputToken || !outputAmount || isZero(outputAmount) || !outputToken) return null; try { - const outputChainId = ethereumUtils.getChainIdFromNetwork(outputToken?.network); + const outputChainId = outputToken.chainId; - const inputChainId = ethereumUtils.getChainIdFromNetwork(inputToken?.network); + const inputChainId = inputToken.chainId; - const inputTokenAddress = isNativeAsset(inputToken?.address, inputChainId) ? ETH_ADDRESS_AGGREGATORS : inputToken?.address; + const inputTokenAddress = isNativeAsset(inputToken.address, inputChainId) ? ETH_ADDRESS_AGGREGATORS : inputToken.address; - const outputTokenAddress = isNativeAsset(outputToken?.address, outputChainId) ? ETH_ADDRESS_AGGREGATORS : outputToken?.address; + const outputTokenAddress = isNativeAsset(outputToken.address, outputChainId) ? ETH_ADDRESS_AGGREGATORS : outputToken.address; const isCrosschainSwap = inputChainId !== outputChainId; if (isCrosschainSwap) return null; @@ -164,10 +164,10 @@ const getOutputAmount = async ( if (!inputAmount || isZero(inputAmount) || !outputToken) return null; try { - const outputChainId = ethereumUtils.getChainIdFromNetwork(outputToken.network); + const outputChainId = outputToken.chainId; const buyTokenAddress = isNativeAsset(outputToken?.address, outputChainId) ? ETH_ADDRESS_AGGREGATORS : outputToken?.address; - const inputChainId = ethereumUtils.getChainIdFromNetwork(inputToken.network); + const inputChainId = inputToken.chainId; const sellTokenAddress = isNativeAsset(inputToken?.address, inputChainId) ? ETH_ADDRESS_AGGREGATORS : inputToken?.address; const sellAmount = convertAmountToRawAmount(convertNumberToString(inputAmount), inputToken.decimals); diff --git a/src/hooks/useSwapInputHandlers.ts b/src/hooks/useSwapInputHandlers.ts index b6c4478bae2..ac7684de5c2 100644 --- a/src/hooks/useSwapInputHandlers.ts +++ b/src/hooks/useSwapInputHandlers.ts @@ -19,12 +19,12 @@ export default function useSwapInputHandlers() { const updateMaxInputAmount = useCallback(() => { const inputCurrencyAddress = inputCurrency?.address; const inputCurrencyUniqueId = inputCurrency?.uniqueId; - const inputCurrencyNetwork = inputCurrency?.network; + const inputCurrencyChainId = inputCurrency?.chainId; const accountAsset = ethereumUtils.getAccountAsset(inputCurrencyUniqueId); const oldAmount = accountAsset?.balance?.amount ?? '0'; let newAmount = oldAmount; - if (isNativeAsset(inputCurrencyAddress, ethereumUtils.getChainIdFromNetwork(inputCurrencyNetwork)) && accountAsset) { + if (isNativeAsset(inputCurrencyAddress, inputCurrencyChainId) && accountAsset) { // this subtracts gas from the balance of the asset newAmount = toFixedDecimals(ethereumUtils.getBalanceAmount(selectedGasFee, accountAsset, l1GasFeeOptimism), 6); @@ -39,7 +39,7 @@ export default function useSwapInputHandlers() { } } dispatch(updateSwapInputAmount(newAmount, true)); - }, [dispatch, inputCurrency?.address, inputCurrency?.network, inputCurrency?.uniqueId, l1GasFeeOptimism, selectedGasFee]); + }, [dispatch, inputCurrency?.address, inputCurrency?.chainId, inputCurrency?.uniqueId, l1GasFeeOptimism, selectedGasFee]); const updateInputAmount = useCallback( (value: string | null) => { diff --git a/src/hooks/useSwapRefuel.ts b/src/hooks/useSwapRefuel.ts index 488edca8bdc..d2455ee51ee 100644 --- a/src/hooks/useSwapRefuel.ts +++ b/src/hooks/useSwapRefuel.ts @@ -7,8 +7,8 @@ import { useEffect, useMemo, useState } from 'react'; import { CROSSCHAIN_SWAPS, useExperimentalFlag } from '@/config'; import { useAccountSettings, useGas } from '.'; import { isNativeAsset } from '@/handlers/assets'; -import { NetworkTypes } from '@/helpers'; import { toWei } from '@/handlers/web3'; +import { ChainId } from '@/networks/types'; export enum RefuelState { 'Add' = 'Add', @@ -32,22 +32,17 @@ export default function useSwapRefuel({ const [outputNativeAsset, setOutputNativeAsset] = useState(); const [inputNativeAsset, setInputNativeAsset] = useState(); - const { inputNetwork, outputNetwork, chainId, toChainId, isCrosschainSwap } = useMemo(() => { - const inputNetwork = inputCurrency.network; - const outputNetwork = outputCurrency.network; - const chainId = ethereumUtils.getChainIdFromNetwork(inputNetwork); - - const toChainId = ethereumUtils.getChainIdFromNetwork(outputNetwork); - const isCrosschainSwap = crosschainSwapsEnabled && inputNetwork !== outputNetwork; + const { chainId, toChainId, isCrosschainSwap } = useMemo(() => { + const chainId = inputCurrency.chainId; + const toChainId = outputCurrency.chainId; + const isCrosschainSwap = crosschainSwapsEnabled && chainId !== toChainId; return { - inputNetwork, - outputNetwork, chainId, toChainId, isCrosschainSwap, }; - }, [crosschainSwapsEnabled, inputCurrency.network, outputCurrency.network]); + }, [crosschainSwapsEnabled, inputCurrency.chainId, outputCurrency.chainId]); const { data: minRefuelAmount } = useMinRefuelAmount( { @@ -59,14 +54,14 @@ export default function useSwapRefuel({ useEffect(() => { const getNativeInputOutputAssets = async () => { - if (!outputNetwork || !inputNetwork || !accountAddress) return; - const outputNativeAsset = await ethereumUtils.getNativeAssetForNetwork(toChainId, accountAddress); - const inputNativeAsset = await ethereumUtils.getNativeAssetForNetwork(chainId, accountAddress); + if (!chainId || !toChainId || !accountAddress) return; + const outputNativeAsset = await ethereumUtils.getNativeAssetForNetwork({ chainId: toChainId, address: accountAddress }); + const inputNativeAsset = await ethereumUtils.getNativeAssetForNetwork({ chainId, address: accountAddress }); setOutputNativeAsset(outputNativeAsset); setInputNativeAsset(inputNativeAsset); }; getNativeInputOutputAssets(); - }, [outputNetwork, inputNetwork, accountAddress, toChainId, chainId]); + }, [accountAddress, toChainId, chainId]); const { showRefuelSheet, refuelState } = useMemo(() => { const swappingToNativeAsset = isNativeAsset(outputCurrency?.address, toChainId); @@ -79,7 +74,7 @@ export default function useSwapRefuel({ return { showRefuelSheet: false, refuelState: null }; } // If we are swapping to mainnet then ignore - if (outputNetwork === NetworkTypes.mainnet) return { showRefuelSheet: false, refuelState: null }; + if (toChainId === ChainId.mainnet) return { showRefuelSheet: false, refuelState: null }; // Does the user have an existing balance on the output native asset const hasZeroOutputNativeAssetBalance = isZero(outputNativeAsset?.balance?.amount || 0); @@ -130,7 +125,6 @@ export default function useSwapRefuel({ minRefuelAmount, outputCurrency?.address, outputNativeAsset?.balance?.amount, - outputNetwork, selectedGasFee?.gasFee?.estimatedFee?.value?.amount, toChainId, tradeDetails?.sellAmount, diff --git a/src/hooks/useSwappableUserAssets.ts b/src/hooks/useSwappableUserAssets.ts index f83282af7bd..7e4338f042d 100644 --- a/src/hooks/useSwappableUserAssets.ts +++ b/src/hooks/useSwappableUserAssets.ts @@ -1,13 +1,13 @@ import { SwappableAsset } from '@/entities'; import { walletFilter } from '@/handlers/tokenSearch'; -import { Network } from '@/helpers'; import { useCoinListEditOptions } from '@/hooks'; import { ETH_ADDRESS } from '@/references'; import { useSortedUserAssets } from '@/resources/assets/useSortedUserAssets'; import { EthereumAddress, ETH_ADDRESS as ETH_ADDRESS_AGGREGATORS } from '@rainbow-me/swaps'; import { ethereumUtils } from '@/utils'; import { useCallback, useEffect, useMemo, useRef } from 'react'; -import { RainbowNetworks, getNetworkObj, getSwappableNetworks } from '@/networks'; +import { RainbowNetworkObjects, getNetworkObject, getSwappableNetworks } from '@/networks'; +import { Network } from '@/networks/types'; type SwappableAddresses = Record; @@ -29,8 +29,7 @@ export const useSwappableUserAssets = (params: { outputCurrency: SwappableAsset if (hiddenCoinsObj[asset.uniqueId]) return true; // filter out networks where swaps are not enabled - const assetNetwork = asset.network; - if (getNetworkObj(assetNetwork).features.swaps) return true; + if (getNetworkObject({ chainId: asset.chainId }).features.swaps) return true; return false; }); @@ -60,7 +59,7 @@ export const useSwappableUserAssets = (params: { outputCurrency: SwappableAsset ); const getSwappableAddressesInWallet = useCallback(async () => { - const networks = RainbowNetworks.filter(({ features }) => features.swaps).map(({ value }) => value); + const networks = RainbowNetworkObjects.filter(({ features }) => features.swaps).map(({ value }) => value); const walletFilterRequests: Promise[] = []; networks.forEach(network => { diff --git a/src/hooks/useTransactionSetup.ts b/src/hooks/useTransactionSetup.ts new file mode 100644 index 00000000000..910a53e9da9 --- /dev/null +++ b/src/hooks/useTransactionSetup.ts @@ -0,0 +1,66 @@ +import * as i18n from '@/languages'; +import { useCallback, useEffect, useState } from 'react'; +import { InteractionManager } from 'react-native'; +import useGas from '@/hooks/useGas'; +import { methodRegistryLookupAndParse } from '@/utils/methodRegistry'; +import { analytics } from '@/analytics'; +import { event } from '@/analytics/event'; +import { RequestSource } from '@/utils/requestNavigationHandlers'; +import { ChainId } from '@/networks/types'; + +type TransactionSetupParams = { + chainId: ChainId; + startPollingGasFees: ReturnType['startPollingGasFees']; + stopPollingGasFees: ReturnType['stopPollingGasFees']; + isMessageRequest: boolean; + transactionDetails: any; + source: RequestSource; +}; + +export const useTransactionSetup = ({ + chainId, + startPollingGasFees, + stopPollingGasFees, + isMessageRequest, + transactionDetails, + source, +}: TransactionSetupParams) => { + const [methodName, setMethodName] = useState(null); + + const fetchMethodName = useCallback( + async (data: string) => { + const methodSignaturePrefix = data.substr(0, 10); + try { + const { name } = await methodRegistryLookupAndParse(methodSignaturePrefix, chainId); + if (name) { + setMethodName(name); + } + } catch (e) { + setMethodName(data); + } + }, + [chainId] + ); + + useEffect(() => { + InteractionManager.runAfterInteractions(() => { + if (chainId) { + if (!isMessageRequest) { + startPollingGasFees(chainId); + fetchMethodName(transactionDetails?.payload?.params?.[0].data); + } else { + setMethodName(i18n.t(i18n.l.wallet.message_signing.request)); + } + analytics.track(event.txRequestShownSheet, { source }); + } + }); + + return () => { + if (!isMessageRequest) { + stopPollingGasFees(); + } + }; + }, [isMessageRequest, chainId, transactionDetails?.payload?.params, source, fetchMethodName, startPollingGasFees, stopPollingGasFees]); + + return { methodName }; +}; diff --git a/src/hooks/useUserAccounts.ts b/src/hooks/useUserAccounts.ts index 4ad20883a51..eafa2508ee9 100644 --- a/src/hooks/useUserAccounts.ts +++ b/src/hooks/useUserAccounts.ts @@ -1,43 +1,37 @@ import { values } from 'lodash'; import useWalletsWithBalancesAndNames from './useWalletsWithBalancesAndNames'; import walletTypes from '@/helpers/walletTypes'; -import { useSelector } from 'react-redux'; -import { AppState } from '@/redux/store'; import { useMemo } from 'react'; import { RainbowAccount } from '@/model/wallet'; -import { Network } from '@/helpers'; export default function useUserAccounts() { const walletsWithBalancesAndNames = useWalletsWithBalancesAndNames(); - const network = useSelector((state: AppState) => state.settings.network); const userAccounts = useMemo(() => { const filteredWallets = values(walletsWithBalancesAndNames).filter(wallet => wallet.type !== walletTypes.readOnly); - const addresses: (RainbowAccount & { network: Network })[] = []; + const addresses: RainbowAccount[] = []; filteredWallets.forEach(wallet => { wallet.addresses?.forEach(account => { addresses.push({ ...account, - network, }); }); }); return addresses; - }, [network, walletsWithBalancesAndNames]); + }, [walletsWithBalancesAndNames]); const watchedAccounts = useMemo(() => { const filteredWallets = values(walletsWithBalancesAndNames).filter(wallet => wallet.type === walletTypes.readOnly); - const addresses: (RainbowAccount & { network: Network })[] = []; + const addresses: RainbowAccount[] = []; filteredWallets.forEach(wallet => { wallet.addresses?.forEach(account => { addresses.push({ ...account, - network, }); }); }); return addresses; - }, [network, walletsWithBalancesAndNames]); + }, [walletsWithBalancesAndNames]); return { userAccounts, diff --git a/src/hooks/useWatchPendingTxs.ts b/src/hooks/useWatchPendingTxs.ts index de1c0ca9bf4..20ddd2169fe 100644 --- a/src/hooks/useWatchPendingTxs.ts +++ b/src/hooks/useWatchPendingTxs.ts @@ -5,10 +5,9 @@ import { userAssetsQueryKey } from '@/resources/assets/UserAssetsQuery'; import { userAssetsQueryKey as swapsUserAssetsQueryKey } from '@/__swaps__/screens/Swap/resources/assets/userAssets'; import { transactionFetchQuery } from '@/resources/transactions/transaction'; import { RainbowError, logger } from '@/logger'; -import { Network } from '@/networks/types'; -import { getIsHardhatConnected, getProviderForNetwork } from '@/handlers/web3'; +import { getProvider } from '@/handlers/web3'; import { consolidatedTransactionsQueryKey } from '@/resources/transactions/consolidatedTransactions'; -import { RainbowNetworks } from '@/networks'; +import { RainbowNetworkObjects } from '@/networks'; import { queryClient } from '@/react-query/queryClient'; import { getTransactionFlashbotStatus } from '@/handlers/transactions'; import { usePendingTransactionsStore } from '@/state/pendingTransactions'; @@ -16,12 +15,16 @@ import { useNonceStore } from '@/state/nonces'; import { Address } from 'viem'; import { nftsQueryKey } from '@/resources/nfts'; import { getNftSortForAddress } from './useNFTsSortBy'; +import { ChainId } from '@/networks/types'; +import { staleBalancesStore } from '@/state/staleBalances'; +import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; export const useWatchPendingTransactions = ({ address }: { address: string }) => { const { storePendingTransactions, setPendingTransactions } = usePendingTransactionsStore(state => ({ storePendingTransactions: state.pendingTransactions, setPendingTransactions: state.setPendingTransactions, })); + const { connectedToHardhat } = useConnectedToHardhatStore(); const setNonce = useNonceStore(state => state.setNonce); @@ -32,7 +35,6 @@ export const useWatchPendingTransactions = ({ address }: { address: string }) => const refreshAssets = useCallback( (_: RainbowTransaction) => { // NOTE: We have two user assets stores right now, so let's invalidate both queries and trigger a refetch - const connectedToHardhat = getIsHardhatConnected(); queryClient.invalidateQueries( userAssetsQueryKey({ address, @@ -49,7 +51,7 @@ export const useWatchPendingTransactions = ({ address }: { address: string }) => ); queryClient.invalidateQueries(nftsQueryKey({ address, sortBy: getNftSortForAddress(address) })); }, - [address, nativeCurrency] + [address, connectedToHardhat, nativeCurrency] ); const processFlashbotsTransaction = useCallback(async (tx: RainbowTransaction): Promise => { @@ -72,7 +74,7 @@ export const useWatchPendingTransactions = ({ address }: { address: string }) => async (tx: RainbowTransaction) => { const transaction = await transactionFetchQuery({ hash: tx.hash!, - network: tx.network, + chainId: tx.chainId, address, currency: nativeCurrency, }); @@ -89,7 +91,7 @@ export const useWatchPendingTransactions = ({ address }: { address: string }) => async (tx: RainbowTransaction) => { let updatedTransaction: RainbowTransaction = { ...tx }; try { - if (tx.network && tx.hash && address) { + if (tx.chainId && tx.hash && address) { updatedTransaction = await processSupportedNetworkTransaction(updatedTransaction); // if flashbots tx and no blockNumber, check if it failed if (!(tx as any).blockNumber && tx.flashbots) { @@ -115,46 +117,46 @@ export const useWatchPendingTransactions = ({ address }: { address: string }) => const processNonces = useCallback( (txs: RainbowTransaction[]) => { const userTxs = txs.filter(tx => address?.toLowerCase() === tx.from?.toLowerCase()); - const networks = [ + const chainIds = [ ...new Set( userTxs.reduce((acc, tx) => { - acc.add(tx.network); + acc.add(tx.chainId); return acc; - }, new Set()) + }, new Set()) ), ]; let flashbotsTxFailed = false; const highestNoncePerChainId = userTxs.reduce((acc, tx) => { // if tx is not on mainnet, we don't care about the nonce - if (tx.network !== Network.mainnet) { - acc.set(tx.network, tx.nonce); + if (tx.chainId !== ChainId.mainnet) { + acc.set(tx.chainId, tx.nonce); return acc; } // if tx is flashbots and failed, we want to use the lowest nonce if (tx.flashbots && (tx as any)?.flashbotsStatus === 'FAILED' && tx?.nonce) { // if we already have a failed flashbots tx, we want to use the lowest nonce - if (flashbotsTxFailed && tx.nonce < acc.get(tx.network)) { - acc.set(tx.network, tx.nonce); + if (flashbotsTxFailed && tx.nonce < acc.get(tx.chainId)) { + acc.set(tx.chainId, tx.nonce); } else { - acc.set(tx.network, tx.nonce); + acc.set(tx.chainId, tx.nonce); flashbotsTxFailed = true; } // if tx succeeded, we want to use the highest nonce - } else if (!flashbotsTxFailed && tx?.nonce && tx.nonce > acc.get(tx.network)) { - acc.set(tx.network, tx.nonce); + } else if (!flashbotsTxFailed && tx?.nonce && tx.nonce > acc.get(tx.chainId)) { + acc.set(tx.chainId, tx.nonce); } return acc; }, new Map()); - networks.map(async network => { - const provider = getProviderForNetwork(network); + chainIds.map(async chainId => { + const provider = getProvider({ chainId }); const providerTransactionCount = await provider.getTransactionCount(address, 'latest'); const currentProviderNonce = providerTransactionCount - 1; - const currentNonceForChainId = highestNoncePerChainId.get(network) - 1; + const currentNonceForChainId = highestNoncePerChainId.get(chainId) - 1; setNonce({ address, - network: network, + chainId, currentNonce: currentProviderNonce > currentNonceForChainId ? currentProviderNonce : currentNonceForChainId, latestConfirmedNonce: currentProviderNonce, }); @@ -187,7 +189,23 @@ export const useWatchPendingTransactions = ({ address }: { address: string }) => ); if (minedTransactions.length) { - const chainIds = RainbowNetworks.filter(network => network.enabled && network.networkType !== 'testnet').map(network => network.id); + const chainIds = RainbowNetworkObjects.filter(networkObject => networkObject.enabled && networkObject.networkType !== 'testnet').map( + networkObject => networkObject.id + ); + minedTransactions.forEach(tx => { + if (tx.changes?.length) { + tx.changes?.forEach(change => { + processStaleAsset({ asset: change?.asset, address, transactionHash: tx?.hash }); + }); + } else if (tx.asset) { + processStaleAsset({ address, asset: tx.asset, transactionHash: tx?.hash }); + } + }); + + queryClient.refetchQueries({ + queryKey: userAssetsQueryKey({ address, currency: nativeCurrency, connectedToHardhat }), + }); + await queryClient.refetchQueries({ queryKey: consolidatedTransactionsQueryKey({ address, @@ -211,7 +229,31 @@ export const useWatchPendingTransactions = ({ address }: { address: string }) => address, pendingTransactions: newPendingTransactions, }); - }, [address, nativeCurrency, pendingTransactions, processNonces, processPendingTransaction, setPendingTransactions]); + }, [address, connectedToHardhat, nativeCurrency, pendingTransactions, processNonces, processPendingTransaction, setPendingTransactions]); return { watchPendingTransactions }; }; + +function processStaleAsset({ + asset, + address, + transactionHash, +}: { + asset: RainbowTransaction['asset']; + address: string; + transactionHash: string; +}) { + const { addStaleBalance } = staleBalancesStore.getState(); + const chainId = asset?.chainId; + if (asset && typeof chainId === 'number') { + const changedAssetAddress = asset?.address as Address; + addStaleBalance({ + address, + chainId, + info: { + address: changedAssetAddress, + transactionHash, + }, + }); + } +} diff --git a/src/languages/en_US.json b/src/languages/en_US.json index 5130d449fc6..31697624327 100644 --- a/src/languages/en_US.json +++ b/src/languages/en_US.json @@ -487,6 +487,7 @@ "clear_local_storage": "Clear local storage", "clear_mmkv_storage": "Clear MMKV storage", "connect_to_hardhat": "Connect to hardhat", + "disconnect_to_hardhat": "Disconnect from hardhat", "crash_app_render_error": "Crash app (render error)", "enable_testnets": "Enable Testnets", "installing_update": "Installing update", @@ -1982,6 +1983,7 @@ "swap": { "actions": { "hold_to_swap": "Hold to Swap", + "hold_to_bridge": "Hold to Bridge", "save": "Save", "enter_amount": "Enter Amount", "review": "Review", @@ -2835,6 +2837,7 @@ "unavailable_personal_sign": "Simulation for personal signs is not yet supported", "unavailable_zora_network": "Simulation on Zora is not yet supported", "failed_to_simulate": "The simulation failed, which suggests your transaction is likely to fail. This may be an issue with the app you’re using.", + "tx_api_error": "We are unable to determine whether or not your transaction will succeed or fail. Proceed with caution.", "warning": "No known malicious behavior was detected, but this transaction has characteristics that may pose a risk to your wallet.", "malicious": "Signing this transaction could result in losing access to everything in your wallet." }, diff --git a/src/migrations/migrations/migrateFavorites.ts b/src/migrations/migrations/migrateFavorites.ts index 46845e56225..4ffa4c52259 100644 --- a/src/migrations/migrations/migrateFavorites.ts +++ b/src/migrations/migrations/migrateFavorites.ts @@ -3,7 +3,6 @@ import { getStandardizedUniqueIdWorklet } from '@/__swaps__/utils/swaps'; import { EthereumAddress, RainbowToken } from '@/entities'; import { createQueryKey, persistOptions, queryClient } from '@/react-query'; import { favoritesQueryKey } from '@/resources/favorites'; -import { ethereumUtils } from '@/utils'; import { persistQueryClientRestore, persistQueryClientSave } from '@tanstack/react-query-persist-client'; import { Migration, MigrationName } from '../types'; @@ -25,7 +24,7 @@ export function migrateFavoritesV2(): Migration { for (const favorite of Object.values(v1Data)) { const uniqueId = getStandardizedUniqueIdWorklet({ address: favorite.address as AddressOrEth, - chainId: ethereumUtils.getChainIdFromNetwork(favorite.network), + chainId: favorite.chainId, }); favorite.uniqueId = uniqueId; // v2 unique uses chainId instead of Network migratedFavorites[uniqueId] = favorite; diff --git a/src/model/remoteConfig.ts b/src/model/remoteConfig.ts index 9974e62ed2e..bf79c65490d 100644 --- a/src/model/remoteConfig.ts +++ b/src/model/remoteConfig.ts @@ -1,4 +1,4 @@ -import { getNetwork, saveNetwork } from '@/handlers/localstorage/globalSettings'; +import { getChainId, saveChainId } from '@/handlers/localstorage/globalSettings'; import { web3SetHttpProvider } from '@/handlers/web3'; import { RainbowError, logger } from '@/logger'; import { createQueryKey, queryClient } from '@/react-query'; @@ -245,9 +245,9 @@ export async function fetchRemoteConfig(): Promise { throw e; } finally { logger.debug(`[remoteConfig]: Current remote config:\n${JSON.stringify(config, null, 2)}`); - const currentNetwork = await getNetwork(); - web3SetHttpProvider(currentNetwork); - saveNetwork(currentNetwork); + const currentChainId = await getChainId(); + web3SetHttpProvider(currentChainId); + saveChainId(currentChainId); } } diff --git a/src/model/wallet.ts b/src/model/wallet.ts index 3271e2f55b6..d35c72f8b48 100644 --- a/src/model/wallet.ts +++ b/src/model/wallet.ts @@ -56,8 +56,8 @@ import { IS_ANDROID } from '@/env'; import { setHardwareTXError } from '@/navigation/HardwareWalletTxNavigator'; import { Signer } from '@ethersproject/abstract-signer'; import { sanitizeTypedData } from '@/utils/signingUtils'; -import { Network } from '@/helpers'; import { ExecuteFnParamsWithoutFn, performanceTracking, Screen } from '@/state/performance/performance'; +import { Network } from '@/networks/types'; export type EthereumPrivateKey = string; type EthereumMnemonic = string; diff --git a/src/navigation/Routes.android.tsx b/src/navigation/Routes.android.tsx index 39d424b8c06..007652f2302 100644 --- a/src/navigation/Routes.android.tsx +++ b/src/navigation/Routes.android.tsx @@ -1,5 +1,5 @@ /* eslint-disable react/jsx-props-no-spreading */ -import { NavigationContainer } from '@react-navigation/native'; +import { NavigationContainer, NavigationContainerRef } from '@react-navigation/native'; import { createStackNavigator } from '@react-navigation/stack'; import React, { useContext } from 'react'; import { AddCashSheet } from '../screens/AddCash'; @@ -90,6 +90,7 @@ import { SwapScreen } from '@/__swaps__/screens/Swap/Swap'; import { useRemoteConfig } from '@/model/remoteConfig'; import { ControlPanel } from '@/components/DappBrowser/control-panel/ControlPanel'; import { ClaimRewardsPanel } from '@/screens/points/claim-flow/ClaimRewardsPanel'; +import { RootStackParamList } from './types'; const Stack = createStackNavigator(); const OuterStack = createStackNavigator(); @@ -269,25 +270,13 @@ function AuthNavigator() { ); } -const AppContainerWithAnalytics = React.forwardRef( - ( - props: { - onReady: () => void; - }, - ref - ) => ( - - - - - - ) -); +const AppContainerWithAnalytics = React.forwardRef, { onReady: () => void }>((props, ref) => ( + + + + + +)); AppContainerWithAnalytics.displayName = 'AppContainerWithAnalytics'; diff --git a/src/navigation/Routes.ios.tsx b/src/navigation/Routes.ios.tsx index c468af96ecb..4f1a8e96407 100644 --- a/src/navigation/Routes.ios.tsx +++ b/src/navigation/Routes.ios.tsx @@ -1,5 +1,5 @@ /* eslint-disable react/jsx-props-no-spreading */ -import { NavigationContainer } from '@react-navigation/native'; +import { NavigationContainer, NavigationContainerRef } from '@react-navigation/native'; import { createStackNavigator } from '@react-navigation/stack'; import React, { useContext } from 'react'; import { AddCashSheet } from '../screens/AddCash'; @@ -104,6 +104,7 @@ import { useRemoteConfig } from '@/model/remoteConfig'; import CheckIdentifierScreen from '@/screens/CheckIdentifierScreen'; import { ControlPanel } from '@/components/DappBrowser/control-panel/ControlPanel'; import { ClaimRewardsPanel } from '@/screens/points/claim-flow/ClaimRewardsPanel'; +import { RootStackParamList } from './types'; const Stack = createStackNavigator(); const NativeStack = createNativeStackNavigator(); @@ -292,25 +293,13 @@ function NativeStackNavigator() { ); } -const AppContainerWithAnalytics = React.forwardRef( - ( - props: { - onReady: () => void; - }, - ref - ) => ( - - - - - - ) -); +const AppContainerWithAnalytics = React.forwardRef, { onReady: () => void }>((props, ref) => ( + + + + + +)); AppContainerWithAnalytics.displayName = 'AppContainerWithAnalytics'; diff --git a/src/navigation/SwipeNavigator.tsx b/src/navigation/SwipeNavigator.tsx index 45fd589fcc6..361fbf5d462 100644 --- a/src/navigation/SwipeNavigator.tsx +++ b/src/navigation/SwipeNavigator.tsx @@ -7,8 +7,7 @@ import { TestnetToast } from '@/components/toasts'; import { DAPP_BROWSER, POINTS, useExperimentalFlag } from '@/config'; import { Box, Columns, globalColors, Stack, useForegroundColor, Text, Cover, useColorMode } from '@/design-system'; import { IS_ANDROID, IS_IOS, IS_TEST } from '@/env'; -import { web3Provider } from '@/handlers/web3'; -import { isUsingButtonNavigation } from '@/helpers/statusBarHelper'; +import { isUsingButtonNavigation } from '@/utils/deviceUtils'; import { useAccountAccentColor, useAccountSettings, useCoinListEdited, useDimensions, usePendingTransactions } from '@/hooks'; import { useRemoteConfig } from '@/model/remoteConfig'; import RecyclerListViewScrollToTopProvider, { @@ -52,7 +51,7 @@ function getTabBarHeight() { return 82; } if (!isUsingButtonNavigation()) { - return 72; + return 82; } return 48; } @@ -446,7 +445,7 @@ function SwipeNavigatorScreens() { } export function SwipeNavigator() { - const { network } = useAccountSettings(); + const { chainId } = useAccountSettings(); const { colors } = useTheme(); return ( @@ -462,7 +461,7 @@ export function SwipeNavigator() { - + ); } diff --git a/src/navigation/config.tsx b/src/navigation/config.tsx index 3e9de2383ff..19663d09165 100644 --- a/src/navigation/config.tsx +++ b/src/navigation/config.tsx @@ -1,14 +1,12 @@ import React from 'react'; -import { Keyboard, StatusBar } from 'react-native'; +import { Keyboard } from 'react-native'; import { useTheme } from '@/theme/ThemeContext'; import colors from '@/theme/currentColors'; import styled from '@/styled-thing'; import { fonts } from '@/styles'; -import networkTypes from '@/helpers/networkTypes'; import WalletBackupStepTypes from '@/helpers/walletBackupStepTypes'; import { deviceUtils, safeAreaInsetValues } from '@/utils'; -import { getNetworkObj } from '@/networks'; import { getPositionSheetHeight } from '@/screens/positions/PositionSheet'; import { Icon } from '@/components/icons'; @@ -29,6 +27,8 @@ import { BottomSheetNavigationOptions } from '@/navigation/bottom-sheet/types'; import { Box } from '@/design-system'; import { IS_ANDROID } from '@/env'; import { SignTransactionSheetRouteProp } from '@/screens/SignTransactionSheet'; +import { RequestSource } from '@/utils/requestNavigationHandlers'; +import { ChainId, chainIdToNameMapping } from '@/networks/types'; export const sharedCoolModalTopOffset = safeAreaInsetValues.top; @@ -277,7 +277,7 @@ export const signTransactionSheetConfig = { options: ({ route }: { route: SignTransactionSheetRouteProp }) => ({ ...buildCoolModalConfig({ ...route.params, - backgroundOpacity: route?.params?.source === 'walletconnect' ? 1 : 0.7, + backgroundOpacity: route?.params?.source === RequestSource.WALLETCONNECT ? 1 : 0.7, cornerRadius: 0, springDamping: 1, topOffset: 0, @@ -490,7 +490,7 @@ export const ensAdditionalRecordsSheetConfig: PartialNavigatorConfigOptions = { }; export const explainSheetConfig: PartialNavigatorConfigOptions = { - options: ({ route: { params = { network: getNetworkObj(networkTypes.mainnet).name } } }) => { + options: ({ route: { params = { network: chainIdToNameMapping[ChainId.mainnet] } } }) => { // @ts-ignore const explainerConfig = explainers(params.network)[params?.type]; return buildCoolModalConfig({ diff --git a/src/networks/README.md b/src/networks/README.md index 47dd785beaa..678269fb209 100644 --- a/src/networks/README.md +++ b/src/networks/README.md @@ -3,9 +3,9 @@ Handling for networks throughout the codebase. ```typescript -import { getNetworkObj, Networks } from '@/networks'; +import { getNetworkObject } from '@/networks'; -const networkObj = getNetworkObj(Networks.mainnet); +const networkObj = getNetworkObject({ chainId: ChainId.mainnet }); // Get static properties based on network const networkName = networkObj.name; @@ -19,10 +19,10 @@ const gasPrices = networkObj.getGasPrices(); // Getting a subset of network objects -const layer2s = RainbowNetworks.filter(network => network.networkType === 'layer2'); +const layer2s = RainbowNetworkObjects.filter(network => network.networkType === 'layer2'); // Or networks that match specific properties -const walletconnectNetworks = RainbowNetworks.filter(network => network.features.walletconnect).map(network => network.value); +const walletconnectNetworks = RainbowNetworkObjects.filter(network => network.features.walletconnect).map(network => network.value); ``` ## Network Objects diff --git a/src/networks/arbitrum.ts b/src/networks/arbitrum.ts index 72c95c4cad5..35c33af556b 100644 --- a/src/networks/arbitrum.ts +++ b/src/networks/arbitrum.ts @@ -1,10 +1,11 @@ -import { getProviderForNetwork, proxyRpcEndpoint } from '@/handlers/web3'; +import { getProvider, proxyRpcEndpoint } from '@/handlers/web3'; import { Network, NetworkProperties } from './types'; import { gasUtils } from '@/utils'; import { arbitrum } from '@wagmi/chains'; import { ARBITRUM_ETH_ADDRESS } from '@/references'; import { getArbitrumGasPrices } from '@/redux/gas'; import { getRemoteConfig } from '@/model/remoteConfig'; +import { ChainId } from '@/networks/types'; export const getArbitrumNetworkObject = (): NetworkProperties => { const { arbitrum_enabled, arbitrum_tx_enabled } = getRemoteConfig(); @@ -25,7 +26,7 @@ export const getArbitrumNetworkObject = (): NetworkProperties => { }, rpc: () => proxyRpcEndpoint(arbitrum.id), - getProvider: () => getProviderForNetwork(Network.arbitrum), + getProvider: () => getProvider({ chainId: ChainId.arbitrum }), balanceCheckerAddress: '0x54A4E5800345c01455a7798E0D96438364e22723', // features diff --git a/src/networks/avalanche.ts b/src/networks/avalanche.ts index ecb8b628d99..cc411357c81 100644 --- a/src/networks/avalanche.ts +++ b/src/networks/avalanche.ts @@ -1,10 +1,11 @@ -import { getProviderForNetwork, proxyRpcEndpoint } from '@/handlers/web3'; +import { getProvider, proxyRpcEndpoint } from '@/handlers/web3'; import { Network, NetworkProperties } from './types'; import { gasUtils } from '@/utils'; import { avalanche } from '@wagmi/chains'; import { AVAX_AVALANCHE_ADDRESS } from '@/references'; import { getAvalancheGasPrices } from '@/redux/gas'; import { getRemoteConfig } from '@/model/remoteConfig'; +import { ChainId } from '@/networks/types'; export const getAvalancheNetworkObject = (): NetworkProperties => { const { avalanche_enabled, avalanche_tx_enabled } = getRemoteConfig(); @@ -26,7 +27,7 @@ export const getAvalancheNetworkObject = (): NetworkProperties => { }, rpc: () => proxyRpcEndpoint(avalanche.id), - getProvider: () => getProviderForNetwork(Network.avalanche), + getProvider: () => getProvider({ chainId: ChainId.avalanche }), // need to find balance checker address balanceCheckerAddress: '', diff --git a/src/networks/base.ts b/src/networks/base.ts index ea2b6163d5a..7a978ddca6d 100644 --- a/src/networks/base.ts +++ b/src/networks/base.ts @@ -1,10 +1,11 @@ -import { getProviderForNetwork, proxyRpcEndpoint } from '@/handlers/web3'; +import { getProvider, proxyRpcEndpoint } from '@/handlers/web3'; import { Network, NetworkProperties } from './types'; import { gasUtils } from '@/utils'; import { base } from '@wagmi/chains'; import { BASE_ETH_ADDRESS } from '@/references'; import { getBaseGasPrices } from '@/redux/gas'; import { getRemoteConfig } from '@/model/remoteConfig'; +import { ChainId } from '@/networks/types'; export const getBaseNetworkObject = (): NetworkProperties => { const { base_enabled, base_tx_enabled, op_chains_enabled, op_chains_tx_enabled } = getRemoteConfig(); @@ -26,7 +27,7 @@ export const getBaseNetworkObject = (): NetworkProperties => { }, rpc: () => proxyRpcEndpoint(base.id), - getProvider: () => getProviderForNetwork(Network.base), + getProvider: () => getProvider({ chainId: ChainId.base }), balanceCheckerAddress: '0x1C8cFdE3Ba6eFc4FF8Dd5C93044B9A690b6CFf36', // features diff --git a/src/networks/blast.ts b/src/networks/blast.ts index f7b168d3e72..6d4d482fc87 100644 --- a/src/networks/blast.ts +++ b/src/networks/blast.ts @@ -1,4 +1,4 @@ -import { getProviderForNetwork, proxyRpcEndpoint } from '@/handlers/web3'; +import { getProvider, proxyRpcEndpoint } from '@/handlers/web3'; import { Network, NetworkProperties } from './types'; import { gasUtils } from '@/utils'; import { blast } from 'viem/chains'; @@ -6,6 +6,7 @@ import { getBlastGasPrices } from '@/redux/gas'; import { getRemoteConfig } from '@/model/remoteConfig'; import { BLAST_MAINNET_RPC } from 'react-native-dotenv'; import { BLAST_ETH_ADDRESS } from '@/references'; +import { ChainId } from '@/networks/types'; export const getBlastNetworkObject = (): NetworkProperties => { const { blast_enabled, blast_tx_enabled } = getRemoteConfig(); @@ -29,7 +30,7 @@ export const getBlastNetworkObject = (): NetworkProperties => { balanceCheckerAddress: '', rpc: () => proxyRpcEndpoint(blast.id), - getProvider: () => getProviderForNetwork(Network.blast), + getProvider: () => getProvider({ chainId: ChainId.blast }), // features features: { diff --git a/src/networks/bsc.ts b/src/networks/bsc.ts index 3e38d897969..89aa63c17d8 100644 --- a/src/networks/bsc.ts +++ b/src/networks/bsc.ts @@ -1,10 +1,11 @@ -import { getProviderForNetwork, proxyRpcEndpoint } from '@/handlers/web3'; +import { getProvider, proxyRpcEndpoint } from '@/handlers/web3'; import { Network, NetworkProperties } from './types'; import { gasUtils } from '@/utils'; import { bsc } from '@wagmi/chains'; import { BNB_BSC_ADDRESS, BNB_MAINNET_ADDRESS } from '@/references'; import { getBscGasPrices } from '@/redux/gas'; import { getRemoteConfig } from '@/model/remoteConfig'; +import { ChainId } from '@/networks/types'; export const getBSCNetworkObject = (): NetworkProperties => { const { bsc_enabled, bsc_tx_enabled } = getRemoteConfig(); @@ -28,7 +29,7 @@ export const getBSCNetworkObject = (): NetworkProperties => { // this should be refactored to have less deps rpc: () => proxyRpcEndpoint(bsc.id), - getProvider: () => getProviderForNetwork(Network.bsc), + getProvider: () => getProvider({ chainId: ChainId.bsc }), balanceCheckerAddress: '0x400A9f1Bb1Db80643C33710C2232A0D74EF5CFf1', // features diff --git a/src/networks/degen.ts b/src/networks/degen.ts index bc7c43aad2d..044e3e6baba 100644 --- a/src/networks/degen.ts +++ b/src/networks/degen.ts @@ -1,4 +1,4 @@ -import { getProviderForNetwork, proxyRpcEndpoint } from '@/handlers/web3'; +import { getProvider, proxyRpcEndpoint } from '@/handlers/web3'; import { Network, NetworkProperties } from './types'; import { gasUtils } from '@/utils'; import { degen } from 'viem/chains'; @@ -6,6 +6,7 @@ import { DEGEN_CHAIN_DEGEN_ADDRESS } from '@/references'; import { getDegenGasPrices } from '@/redux/gas'; import { getRemoteConfig } from '@/model/remoteConfig'; import { DEGEN_MAINNET_RPC } from 'react-native-dotenv'; +import { ChainId } from '@/networks/types'; export const getDegenNetworkObject = (): NetworkProperties => { const { degen_enabled, degen_tx_enabled } = getRemoteConfig(); @@ -28,7 +29,7 @@ export const getDegenNetworkObject = (): NetworkProperties => { }, rpc: () => proxyRpcEndpoint(degen.id), - getProvider: () => getProviderForNetwork(Network.degen), + getProvider: () => getProvider({ chainId: ChainId.degen }), // need to find balance checker address balanceCheckerAddress: '', diff --git a/src/networks/gnosis.ts b/src/networks/gnosis.ts index f6e18c742b7..cac594c0e13 100644 --- a/src/networks/gnosis.ts +++ b/src/networks/gnosis.ts @@ -1,9 +1,10 @@ -import { getProviderForNetwork } from '@/handlers/web3'; +import { getProvider } from '@/handlers/web3'; import { Network, NetworkProperties } from './types'; import { gasUtils } from '@/utils'; import { gnosis } from '@wagmi/chains'; import { ETH_ADDRESS } from '@/references'; import { getOptimismGasPrices } from '@/redux/gas'; +import { ChainId } from '@/networks/types'; export const getGnosisNetworkObject = (): NetworkProperties => { return { @@ -24,7 +25,7 @@ export const getGnosisNetworkObject = (): NetworkProperties => { }, rpc: () => '', - getProvider: () => getProviderForNetwork(Network.optimism), + getProvider: () => getProvider({ chainId: ChainId.gnosis }), balanceCheckerAddress: '', // features diff --git a/src/networks/goerli.ts b/src/networks/goerli.ts index 2d9423a7ac2..71d3e19aeca 100644 --- a/src/networks/goerli.ts +++ b/src/networks/goerli.ts @@ -1,9 +1,10 @@ -import { getProviderForNetwork, proxyRpcEndpoint } from '@/handlers/web3'; +import { getProvider, proxyRpcEndpoint } from '@/handlers/web3'; import { Network, NetworkProperties } from './types'; import { gasUtils } from '@/utils'; import { goerli } from '@wagmi/chains'; import { ETH_ADDRESS } from '@/references'; import { getRemoteConfig } from '@/model/remoteConfig'; +import { ChainId } from '@/networks/types'; export const getGoerliNetworkObject = (): NetworkProperties => { const { goerli_enabled, goerli_tx_enabled } = getRemoteConfig(); @@ -25,7 +26,7 @@ export const getGoerliNetworkObject = (): NetworkProperties => { }, // this should be refactored to have less deps - getProvider: () => getProviderForNetwork(Network.goerli), + getProvider: () => getProvider({ chainId: ChainId.goerli }), rpc: () => proxyRpcEndpoint(goerli.id), balanceCheckerAddress: '0xf3352813b612a2d198e437691557069316b84ebe', diff --git a/src/networks/index.ts b/src/networks/index.ts index 79c9089f23a..b4c5dd2f1f6 100644 --- a/src/networks/index.ts +++ b/src/networks/index.ts @@ -1,4 +1,4 @@ -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; import store from '@/redux/store'; import * as ls from '@/storage'; import { getArbitrumNetworkObject } from './arbitrum'; @@ -12,14 +12,14 @@ import { getGoerliNetworkObject } from './goerli'; import { getMainnetNetworkObject } from './mainnet'; import { getOptimismNetworkObject } from './optimism'; import { getPolygonNetworkObject } from './polygon'; -import { Network, NetworkProperties } from './types'; +import { NetworkProperties } from './types'; import { getZoraNetworkObject } from './zora'; /** * Array of all Rainbow Networks * the ordering is the default sorting */ -export const RainbowNetworks = [ +export const RainbowNetworkObjects = [ getMainnetNetworkObject(), getArbitrumNetworkObject(), getBaseNetworkObject(), @@ -34,46 +34,24 @@ export const RainbowNetworks = [ getDegenNetworkObject(), ]; +export const RainbowSupportedChainIds = [ + ChainId.mainnet, + ChainId.arbitrum, + ChainId.base, + ChainId.optimism, + ChainId.polygon, + ChainId.zora, + ChainId.gnosis, + ChainId.goerli, + ChainId.bsc, + ChainId.avalanche, + ChainId.blast, + ChainId.degen, +]; + /** * Helper function to get specific Rainbow Network's Object */ -export function getNetworkObj(network: Network): NetworkProperties { - switch (network) { - // Mainnet - case Network.mainnet: - return getMainnetNetworkObject(); - - // L2s - case Network.arbitrum: - return getArbitrumNetworkObject(); - case Network.base: - return getBaseNetworkObject(); - case Network.bsc: - return getBSCNetworkObject(); - case Network.optimism: - return getOptimismNetworkObject(); - case Network.polygon: - return getPolygonNetworkObject(); - case Network.zora: - return getZoraNetworkObject(); - case Network.gnosis: - return getGnosisNetworkObject(); - case Network.avalanche: - return getAvalancheNetworkObject(); - case Network.blast: - return getBlastNetworkObject(); - case Network.degen: - return getDegenNetworkObject(); - // Testnets - case Network.goerli: - return getGoerliNetworkObject(); - - // Fallback - default: - return getMainnetNetworkObject(); - } -} - export function getNetworkObject({ chainId }: { chainId: ChainId }): NetworkProperties { switch (chainId) { // Mainnet @@ -125,14 +103,14 @@ export function sortNetworks(): NetworkProperties[] { return count1 > count2 ? -1 : 1; }; - return RainbowNetworks.sort(tokenSort); + return RainbowNetworkObjects.sort(tokenSort); } export function getSwappableNetworks(): NetworkProperties[] { - return RainbowNetworks.filter(network => network.features.swaps); + return RainbowNetworkObjects.filter(network => network.features.swaps); } -export const RainbowNetworkByChainId = RainbowNetworks.reduce( +export const RainbowNetworkByChainId = RainbowNetworkObjects.reduce( (acc, network) => { acc[network.id] = network; return acc; diff --git a/src/networks/mainnet.ts b/src/networks/mainnet.ts index 2c3644511ec..e185b5c1b25 100644 --- a/src/networks/mainnet.ts +++ b/src/networks/mainnet.ts @@ -1,9 +1,10 @@ -import { getProviderForNetwork, proxyRpcEndpoint } from '@/handlers/web3'; -import { Network, NetworkProperties } from './types'; +import { getProvider, proxyRpcEndpoint } from '@/handlers/web3'; +import { Network, NetworkProperties, ChainId } from './types'; import { gasUtils } from '@/utils'; import { mainnet } from '@wagmi/chains'; import { ETH_ADDRESS } from '@/references'; import { getRemoteConfig } from '@/model/remoteConfig'; +import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; export const getMainnetNetworkObject = (): NetworkProperties => { const { mainnet_enabled, mainnet_tx_enabled } = getRemoteConfig(); @@ -24,9 +25,8 @@ export const getMainnetNetworkObject = (): NetworkProperties => { address: ETH_ADDRESS, }, - // this should be refactored to have less deps - getProvider: () => getProviderForNetwork(Network.mainnet), - rpc: () => proxyRpcEndpoint(mainnet.id), + getProvider: () => getProvider({ chainId: ChainId.mainnet }), + rpc: () => (useConnectedToHardhatStore.getState().connectedToHardhat ? 'http://127.0.0.1:8545' : proxyRpcEndpoint(mainnet.id)), balanceCheckerAddress: '0x4dcf4562268dd384fe814c00fad239f06c2a0c2b', // features diff --git a/src/networks/optimism.ts b/src/networks/optimism.ts index b2d6ce8c8a6..2a9bf3c4884 100644 --- a/src/networks/optimism.ts +++ b/src/networks/optimism.ts @@ -1,10 +1,11 @@ -import { getProviderForNetwork, proxyRpcEndpoint } from '@/handlers/web3'; +import { getProvider, proxyRpcEndpoint } from '@/handlers/web3'; import { Network, NetworkProperties } from './types'; import { gasUtils } from '@/utils'; import { optimism } from '@wagmi/chains'; import { OPTIMISM_ETH_ADDRESS } from '@/references'; import { getOptimismGasPrices } from '@/redux/gas'; import { getRemoteConfig } from '@/model/remoteConfig'; +import { ChainId } from '@/networks/types'; export const getOptimismNetworkObject = (): NetworkProperties => { const { optimism_enabled, optimism_tx_enabled, op_chains_enabled, op_chains_tx_enabled } = getRemoteConfig(); @@ -26,7 +27,7 @@ export const getOptimismNetworkObject = (): NetworkProperties => { }, rpc: () => proxyRpcEndpoint(optimism.id), - getProvider: () => getProviderForNetwork(Network.optimism), + getProvider: () => getProvider({ chainId: ChainId.optimism }), balanceCheckerAddress: '0x1C8cFdE3Ba6eFc4FF8Dd5C93044B9A690b6CFf36', // features diff --git a/src/networks/polygon.ts b/src/networks/polygon.ts index 7866a77a4c4..49f4feba581 100644 --- a/src/networks/polygon.ts +++ b/src/networks/polygon.ts @@ -1,10 +1,11 @@ -import { getProviderForNetwork, proxyRpcEndpoint } from '@/handlers/web3'; +import { getProvider, proxyRpcEndpoint } from '@/handlers/web3'; import { Network, NetworkProperties } from './types'; import { gasUtils } from '@/utils'; import { polygon } from '@wagmi/chains'; import { MATIC_MAINNET_ADDRESS, MATIC_POLYGON_ADDRESS } from '@/references'; import { getPolygonGasPrices } from '@/redux/gas'; import { getRemoteConfig } from '@/model/remoteConfig'; +import { ChainId } from '@/networks/types'; export const getPolygonNetworkObject = (): NetworkProperties => { const { polygon_tx_enabled } = getRemoteConfig(); @@ -27,7 +28,7 @@ export const getPolygonNetworkObject = (): NetworkProperties => { }, rpc: () => proxyRpcEndpoint(polygon.id), - getProvider: () => getProviderForNetwork(Network.polygon), + getProvider: () => getProvider({ chainId: ChainId.polygon }), balanceCheckerAddress: '0x54A4E5800345c01455a77798E0D96438364e22723', // features diff --git a/src/networks/types.ts b/src/networks/types.ts index 052080ce5c7..6e94851fc28 100644 --- a/src/networks/types.ts +++ b/src/networks/types.ts @@ -2,7 +2,11 @@ import { EthereumAddress } from '@/entities'; import { GasPricesAPIData } from '@/entities/gas'; import { StaticJsonRpcProvider } from '@ethersproject/providers'; import { Chain } from '@wagmi/chains'; -// network.ts +import * as chain from 'viem/chains'; + +const HARDHAT_CHAIN_ID = 1337; +const HARDHAT_OP_CHAIN_ID = 1338; + export enum Network { arbitrum = 'arbitrum', goerli = 'goerli', @@ -18,6 +22,199 @@ export enum Network { degen = 'degen', } +export enum ChainId { + arbitrum = chain.arbitrum.id, + arbitrumNova = chain.arbitrumNova.id, + arbitrumSepolia = chain.arbitrumSepolia.id, + avalanche = chain.avalanche.id, + avalancheFuji = chain.avalancheFuji.id, + base = chain.base.id, + baseSepolia = chain.baseSepolia.id, + blast = chain.blast.id, + blastSepolia = chain.blastSepolia.id, + bsc = chain.bsc.id, + bscTestnet = chain.bscTestnet.id, + celo = chain.celo.id, + degen = chain.degen.id, + gnosis = chain.gnosis.id, + goerli = chain.goerli.id, + hardhat = HARDHAT_CHAIN_ID, + hardhatOptimism = HARDHAT_OP_CHAIN_ID, + holesky = chain.holesky.id, + linea = chain.linea.id, + mainnet = chain.mainnet.id, + manta = chain.manta.id, + optimism = chain.optimism.id, + optimismSepolia = chain.optimismSepolia.id, + polygon = chain.polygon.id, + polygonAmoy = chain.polygonAmoy.id, + polygonMumbai = chain.polygonMumbai.id, + polygonZkEvm = chain.polygonZkEvm.id, + rari = 1380012617, + scroll = chain.scroll.id, + sepolia = chain.sepolia.id, + zora = chain.zora.id, + zoraSepolia = chain.zoraSepolia.id, +} + +export enum ChainName { + arbitrum = 'arbitrum', + arbitrumNova = 'arbitrum-nova', + arbitrumSepolia = 'arbitrum-sepolia', + avalanche = 'avalanche', + avalancheFuji = 'avalanche-fuji', + base = 'base', + blast = 'blast', + blastSepolia = 'blast-sepolia', + bsc = 'bsc', + celo = 'celo', + degen = 'degen', + gnosis = 'gnosis', + goerli = 'goerli', + linea = 'linea', + manta = 'manta', + optimism = 'optimism', + polygon = 'polygon', + polygonZkEvm = 'polygon-zkevm', + rari = 'rari', + scroll = 'scroll', + zora = 'zora', + mainnet = 'mainnet', + holesky = 'holesky', + hardhat = 'hardhat', + hardhatOptimism = 'hardhat-optimism', + sepolia = 'sepolia', + optimismSepolia = 'optimism-sepolia', + bscTestnet = 'bsc-testnet', + polygonMumbai = 'polygon-mumbai', + baseSepolia = 'base-sepolia', + zoraSepolia = 'zora-sepolia', + polygonAmoy = 'polygon-amoy', +} + +export const networkToIdMapping: { [key in Network]: ChainId } = { + [Network.arbitrum]: ChainId.arbitrum, + [Network.goerli]: ChainId.goerli, + [Network.mainnet]: ChainId.mainnet, + [Network.optimism]: ChainId.optimism, + [Network.polygon]: ChainId.polygon, + [Network.base]: ChainId.base, + [Network.bsc]: ChainId.bsc, + [Network.zora]: ChainId.zora, + [Network.gnosis]: ChainId.gnosis, + [Network.avalanche]: ChainId.avalanche, + [Network.blast]: ChainId.blast, + [Network.degen]: ChainId.degen, +}; + +export const chainNameToIdMapping: { + [key in ChainName | 'ethereum' | 'ethereum-sepolia']: ChainId; +} = { + ['ethereum']: ChainId.mainnet, + [ChainName.arbitrum]: ChainId.arbitrum, + [ChainName.arbitrumNova]: ChainId.arbitrumNova, + [ChainName.arbitrumSepolia]: ChainId.arbitrumSepolia, + [ChainName.avalanche]: ChainId.avalanche, + [ChainName.avalancheFuji]: ChainId.avalancheFuji, + [ChainName.base]: ChainId.base, + [ChainName.bsc]: ChainId.bsc, + [ChainName.celo]: ChainId.celo, + [ChainName.degen]: ChainId.degen, + [ChainName.gnosis]: ChainId.gnosis, + [ChainName.linea]: ChainId.linea, + [ChainName.manta]: ChainId.manta, + [ChainName.optimism]: ChainId.optimism, + [ChainName.goerli]: ChainId.goerli, + [ChainName.polygon]: ChainId.polygon, + [ChainName.polygonZkEvm]: ChainId.polygonZkEvm, + [ChainName.rari]: ChainId.rari, + [ChainName.scroll]: ChainId.scroll, + [ChainName.zora]: ChainId.zora, + [ChainName.mainnet]: ChainId.mainnet, + [ChainName.holesky]: ChainId.holesky, + [ChainName.hardhat]: ChainId.hardhat, + [ChainName.hardhatOptimism]: ChainId.hardhatOptimism, + ['ethereum-sepolia']: ChainId.sepolia, + [ChainName.sepolia]: ChainId.sepolia, + [ChainName.optimismSepolia]: ChainId.optimismSepolia, + [ChainName.bscTestnet]: ChainId.bscTestnet, + [ChainName.polygonMumbai]: ChainId.polygonMumbai, + [ChainName.baseSepolia]: ChainId.baseSepolia, + [ChainName.zoraSepolia]: ChainId.zoraSepolia, + [ChainName.blast]: ChainId.blast, + [ChainName.blastSepolia]: ChainId.blastSepolia, + [ChainName.polygonAmoy]: ChainId.polygonAmoy, +}; + +export const chainIdToNameMapping: { + [key in ChainId]: ChainName; +} = { + [ChainId.arbitrum]: ChainName.arbitrum, + [ChainId.arbitrumNova]: ChainName.arbitrumNova, + [ChainId.arbitrumSepolia]: ChainName.arbitrumSepolia, + [ChainId.avalanche]: ChainName.avalanche, + [ChainId.avalancheFuji]: ChainName.avalancheFuji, + [ChainId.base]: ChainName.base, + [ChainId.blast]: ChainName.blast, + [ChainId.blastSepolia]: ChainName.blastSepolia, + [ChainId.bsc]: ChainName.bsc, + [ChainId.celo]: ChainName.celo, + [ChainId.degen]: ChainName.degen, + [ChainId.gnosis]: ChainName.gnosis, + [ChainId.linea]: ChainName.linea, + [ChainId.manta]: ChainName.manta, + [ChainId.optimism]: ChainName.optimism, + [ChainId.polygon]: ChainName.polygon, + [ChainId.polygonZkEvm]: ChainName.polygonZkEvm, + [ChainId.rari]: ChainName.rari, + [ChainId.scroll]: ChainName.scroll, + [ChainId.zora]: ChainName.zora, + [ChainId.mainnet]: ChainName.mainnet, + [ChainId.holesky]: ChainName.holesky, + [ChainId.hardhat]: ChainName.hardhat, + [ChainId.hardhatOptimism]: ChainName.hardhatOptimism, + [ChainId.sepolia]: ChainName.sepolia, + [ChainId.optimismSepolia]: ChainName.optimismSepolia, + [ChainId.bscTestnet]: ChainName.bscTestnet, + [ChainId.polygonMumbai]: ChainName.polygonMumbai, + [ChainId.baseSepolia]: ChainName.baseSepolia, + [ChainId.zoraSepolia]: ChainName.zoraSepolia, + [ChainId.polygonAmoy]: ChainName.polygonAmoy, +}; + +export const ChainNameDisplay = { + [ChainId.arbitrum]: 'Arbitrum', + [ChainId.arbitrumNova]: chain.arbitrumNova.name, + [ChainId.avalanche]: 'Avalanche', + [ChainId.avalancheFuji]: 'Avalanche Fuji', + [ChainId.base]: 'Base', + [ChainId.blast]: 'Blast', + [ChainId.blastSepolia]: 'Blast Sepolia', + [ChainId.bsc]: 'BSC', + [ChainId.celo]: chain.celo.name, + [ChainId.degen]: 'Degen Chain', + [ChainId.linea]: 'Linea', + [ChainId.manta]: 'Manta', + [ChainId.optimism]: 'Optimism', + [ChainId.polygon]: 'Polygon', + [ChainId.polygonZkEvm]: chain.polygonZkEvm.name, + [ChainId.rari]: 'RARI Chain', + [ChainId.scroll]: chain.scroll.name, + [ChainId.zora]: 'Zora', + [ChainId.mainnet]: 'Ethereum', + [ChainId.hardhat]: 'Hardhat', + [ChainId.hardhatOptimism]: 'Hardhat OP', + [ChainId.sepolia]: chain.sepolia.name, + [ChainId.holesky]: chain.holesky.name, + [ChainId.optimismSepolia]: chain.optimismSepolia.name, + [ChainId.bscTestnet]: 'BSC Testnet', + [ChainId.polygonMumbai]: chain.polygonMumbai.name, + [ChainId.arbitrumSepolia]: chain.arbitrumSepolia.name, + [ChainId.baseSepolia]: chain.baseSepolia.name, + [ChainId.zoraSepolia]: 'Zora Sepolia', + [ChainId.polygonAmoy]: 'Polygon Amoy', +} as const; + export type NetworkTypes = 'layer1' | 'layer2' | 'testnet'; export interface NetworkProperties extends Chain { diff --git a/src/networks/zora.ts b/src/networks/zora.ts index e8c5da58ffd..58d95526fdc 100644 --- a/src/networks/zora.ts +++ b/src/networks/zora.ts @@ -1,10 +1,11 @@ -import { getProviderForNetwork, proxyRpcEndpoint } from '@/handlers/web3'; +import { getProvider, proxyRpcEndpoint } from '@/handlers/web3'; import { Network, NetworkProperties } from './types'; import { gasUtils } from '@/utils'; import { zora } from '@wagmi/chains'; import { ZORA_ETH_ADDRESS } from '@/references'; import { getZoraGasPrices } from '@/redux/gas'; import { getRemoteConfig } from '@/model/remoteConfig'; +import { ChainId } from '@/networks/types'; export const getZoraNetworkObject = (): NetworkProperties => { const { zora_enabled, zora_tx_enabled, op_chains_enabled, op_chains_tx_enabled } = getRemoteConfig(); @@ -26,7 +27,7 @@ export const getZoraNetworkObject = (): NetworkProperties => { }, rpc: () => proxyRpcEndpoint(zora.id), - getProvider: () => getProviderForNetwork(Network.zora), + getProvider: () => getProvider({ chainId: ChainId.arbitrum }), balanceCheckerAddress: '0x1C8cFdE3Ba6eFc4FF8Dd5C93044B9A690b6CFf36', // features diff --git a/src/notifications/NotificationsHandler.tsx b/src/notifications/NotificationsHandler.tsx index 5c0d5a41af0..bb5240980db 100644 --- a/src/notifications/NotificationsHandler.tsx +++ b/src/notifications/NotificationsHandler.tsx @@ -22,7 +22,7 @@ import { Navigation } from '@/navigation'; import Routes from '@rainbow-me/routes'; import { AppState as ApplicationState, AppStateStatus, NativeEventSubscription } from 'react-native'; import notifee, { Event as NotifeeEvent, EventType } from '@notifee/react-native'; -import { ethereumUtils, isLowerCaseMatch } from '@/utils'; +import { isLowerCaseMatch } from '@/utils'; import walletTypes from '@/helpers/walletTypes'; import { NotificationSubscriptionChangesListener, @@ -165,10 +165,11 @@ export const NotificationsHandler = ({ walletReady }: Props) => { } Navigation.handleAction(Routes.PROFILE_SCREEN, {}); - const network = ethereumUtils.getNetworkFromChainId(parseInt(data.chain, 10)); + const chainId = parseInt(data.chain, 10); + const transaction = await transactionFetchQuery({ hash: data.hash, - network: network, + chainId, address: walletAddress, currency: nativeCurrency, }); diff --git a/src/notifications/tokens.ts b/src/notifications/tokens.ts index 8c88b9ee1bf..4bc8b78a88d 100644 --- a/src/notifications/tokens.ts +++ b/src/notifications/tokens.ts @@ -2,7 +2,7 @@ import messaging from '@react-native-firebase/messaging'; import { getLocal, saveLocal } from '@/handlers/localstorage/common'; import { getPermissionStatus } from '@/notifications/permissions'; -import { logger, RainbowError } from '@/logger'; +import { logger } from '@/logger'; export const registerTokenRefreshListener = () => messaging().onTokenRefresh(fcmToken => { diff --git a/src/parsers/accounts.js b/src/parsers/accounts.js deleted file mode 100644 index aa31cce971b..00000000000 --- a/src/parsers/accounts.js +++ /dev/null @@ -1,72 +0,0 @@ -import isNil from 'lodash/isNil'; -import toUpper from 'lodash/toUpper'; -import { isNativeAsset } from '@/handlers/assets'; -import * as i18n from '@/languages'; -import { convertAmountAndPriceToNativeDisplay, convertAmountToNativeDisplay, convertAmountToPercentageDisplay } from '@/helpers/utilities'; -import { getTokenMetadata, isLowerCaseMatch } from '@/utils'; -import { memoFn } from '@/utils/memoFn'; -import { getUniqueId } from '@/utils/ethereumUtils'; -import { ChainId } from '@/__swaps__/types/chains'; - -// eslint-disable-next-line no-useless-escape -const sanitize = memoFn(s => s.replace(/[^a-z0-9áéíóúñü \.,_@:-]/gim, '')); - -export const parseAssetName = (metadata, name) => { - if (metadata?.name) return metadata?.name; - return name ? sanitize(name) : i18n.t(i18n.l.assets.unkown_token); -}; - -export const parseAssetSymbol = (metadata, symbol) => { - if (metadata?.symbol) return metadata?.symbol; - return symbol ? toUpper(sanitize(symbol)) : '———'; -}; - -/** - * @desc parse asset - * @param {Object} assetData - * @return The parsed asset. - */ -export const parseAsset = ({ asset_code: address, ...asset } = {}) => { - const metadata = getTokenMetadata(asset.mainnet_address || address); - const name = parseAssetName(metadata, asset.name); - const symbol = parseAssetSymbol(metadata, asset.symbol); - - const parsedAsset = { - ...asset, - ...metadata, - address, - isNativeAsset: isNativeAsset(address, asset.chain_id || ChainId.mainnet), - name, - symbol, - uniqueId: getUniqueId(address, asset.chain_id), - }; - - return parsedAsset; -}; - -export const parseAssetsNative = (assets, nativeCurrency) => assets.map(asset => parseAssetNative(asset, nativeCurrency)); - -export const parseAssetNative = (asset, nativeCurrency) => { - const assetNativePrice = asset?.price; - if (isNil(assetNativePrice)) { - return asset; - } - - const priceUnit = assetNativePrice?.value ?? 0; - const nativeDisplay = convertAmountAndPriceToNativeDisplay(asset?.balance?.amount ?? 0, priceUnit, nativeCurrency); - return { - ...asset, - native: { - balance: nativeDisplay, - change: isLowerCaseMatch(asset.symbol, nativeCurrency) - ? null - : assetNativePrice.relative_change_24h - ? convertAmountToPercentageDisplay(assetNativePrice.relative_change_24h) - : '', - price: { - amount: priceUnit, - display: convertAmountToNativeDisplay(priceUnit, nativeCurrency), - }, - }, - }; -}; diff --git a/src/parsers/accounts.ts b/src/parsers/accounts.ts new file mode 100644 index 00000000000..279e7eaa89a --- /dev/null +++ b/src/parsers/accounts.ts @@ -0,0 +1,32 @@ +import isNil from 'lodash/isNil'; +import { convertAmountAndPriceToNativeDisplay, convertAmountToNativeDisplay, convertAmountToPercentageDisplay } from '@/helpers/utilities'; +import { isLowerCaseMatch } from '@/utils'; +import { NativeCurrencyKey, ParsedAddressAsset } from '@/entities'; + +export const parseAssetsNative = (assets: ParsedAddressAsset[], nativeCurrency: NativeCurrencyKey) => + assets.map(asset => parseAssetNative(asset, nativeCurrency)); + +export const parseAssetNative = (asset: ParsedAddressAsset, nativeCurrency: NativeCurrencyKey) => { + const assetNativePrice = asset?.price; + if (isNil(assetNativePrice)) { + return asset; + } + + const priceUnit = assetNativePrice?.value ?? 0; + const nativeDisplay = convertAmountAndPriceToNativeDisplay(asset?.balance?.amount ?? 0, priceUnit, nativeCurrency); + return { + ...asset, + native: { + balance: nativeDisplay, + change: isLowerCaseMatch(asset.symbol, nativeCurrency) + ? undefined + : assetNativePrice.relative_change_24h + ? convertAmountToPercentageDisplay(assetNativePrice.relative_change_24h) + : '', + price: { + amount: priceUnit?.toString(), + display: convertAmountToNativeDisplay(priceUnit, nativeCurrency), + }, + }, + }; +}; diff --git a/src/parsers/gas.ts b/src/parsers/gas.ts index 01058c09b71..ce48764e84b 100644 --- a/src/parsers/gas.ts +++ b/src/parsers/gas.ts @@ -33,7 +33,6 @@ import { multiply, toFixedDecimals, } from '@/helpers/utilities'; -import { Network } from '@/networks/types'; type BigNumberish = number | string | BigNumber; @@ -98,8 +97,7 @@ const parseGasDataConfirmationTime = ( }; export const parseRainbowMeteorologyData = ( - rainbowMeterologyData: RainbowMeteorologyData, - network: Network + rainbowMeterologyData: RainbowMeteorologyData ): { gasFeeParamsBySpeed: GasFeeParamsBySpeed; baseFeePerGas: GasFeeParam; diff --git a/src/parsers/index.ts b/src/parsers/index.ts index 1f520d23bde..705b2cde5c0 100644 --- a/src/parsers/index.ts +++ b/src/parsers/index.ts @@ -1,4 +1,4 @@ -export { parseAssetName, parseAssetSymbol, parseAsset, parseAssetNative, parseAssetsNative } from './accounts'; +export { parseAssetNative, parseAssetsNative } from './accounts'; export { parseL2GasPrices, parseGasFeesBySpeed, diff --git a/src/parsers/requests.js b/src/parsers/requests.js index ae7da2d87b6..9b8747334d8 100644 --- a/src/parsers/requests.js +++ b/src/parsers/requests.js @@ -9,7 +9,7 @@ import { isSignTypedData, SIGN, PERSONAL_SIGN, SEND_TRANSACTION, SIGN_TRANSACTIO import { isAddress } from '@ethersproject/address'; import { toUtf8String } from '@ethersproject/strings'; -export const getRequestDisplayDetails = (payload, nativeCurrency, chainId) => { +export const getRequestDisplayDetails = async (payload, nativeCurrency, chainId) => { const timestampInMs = Date.now(); if (payload.method === SEND_TRANSACTION || payload.method === SIGN_TRANSACTION) { const transaction = Object.assign(payload?.params?.[0] ?? null); @@ -75,9 +75,9 @@ const getMessageDisplayDetails = (message, timestampInMs) => ({ timestampInMs, }); -const getTransactionDisplayDetails = (transaction, nativeCurrency, timestampInMs, chainId) => { +const getTransactionDisplayDetails = async (transaction, nativeCurrency, timestampInMs, chainId) => { const tokenTransferHash = smartContractMethods.token_transfer.hash; - const nativeAsset = ethereumUtils.getNativeAssetForNetwork(chainId); + const nativeAsset = await ethereumUtils.getNativeAssetForNetwork({ chainId }); if (transaction.data === '0x') { const value = fromWei(convertHexToString(transaction.value)); const priceUnit = nativeAsset?.price?.value ?? 0; diff --git a/src/parsers/transactions.ts b/src/parsers/transactions.ts index 495ff7ba0c4..712b8de45b7 100644 --- a/src/parsers/transactions.ts +++ b/src/parsers/transactions.ts @@ -20,7 +20,7 @@ import { TransactionType, TransactionWithChangesType, } from '@/resources/transactions/types'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; const LAST_TXN_HASH_BUFFER = 20; @@ -113,10 +113,16 @@ export const parseTransaction = async ( iconUrl: meta.contract_icon_url, }; + // NOTE: For send transactions, the to address should be pulled from the outgoing change directly, not the txn.address_to + let to = txn.address_to; + if (meta.type === 'send') { + to = txn.changes.find(change => change?.direction === 'out')?.address_to ?? txn.address_to; + } + return { chainId, from: txn.address_from, - to: txn.address_to, + to, title: `${type}.${status}`, description, hash, diff --git a/src/raps/actions/claimBridge.ts b/src/raps/actions/claimBridge.ts index 208f337024f..5143fd2e922 100644 --- a/src/raps/actions/claimBridge.ts +++ b/src/raps/actions/claimBridge.ts @@ -1,6 +1,5 @@ -import { NewTransaction, TransactionGasParamAmounts } from '@/entities'; -import { getProviderForNetwork } from '@/handlers/web3'; -import { Network } from '@/helpers'; +import { NewTransaction, ParsedAddressAsset, TransactionGasParamAmounts } from '@/entities'; +import { getProvider } from '@/handlers/web3'; import { add, addBuffer, greaterThan, lessThan, multiply, subtract } from '@/helpers/utilities'; import { RainbowError } from '@/logger'; import store from '@/redux/store'; @@ -13,6 +12,7 @@ import { CrosschainQuote, QuoteError, SwapType, getClaimBridgeQuote } from '@rai import { Address } from 'viem'; import { ActionProps } from '../references'; import { executeCrosschainSwap } from './crosschainSwap'; +import { ChainId } from '@/networks/types'; // This action is used to bridge the claimed funds to another chain export async function claimBridge({ parameters, wallet, baseNonce }: ActionProps<'claimBridge'>) { @@ -51,7 +51,7 @@ export async function claimBridge({ parameters, wallet, baseNonce }: ActionProps // 2 - We use the default gas limit (already inflated) from the quote to calculate the aproximate gas fee const initalGasLimit = bridgeQuote.defaultGasLimit as string; - const provider = getProviderForNetwork(Network.optimism); + const provider = getProvider({ chainId: ChainId.optimism }); const l1GasFeeOptimism = await ethereumUtils.calculateL1FeeOptimism( // @ts-ignore @@ -153,17 +153,21 @@ export async function claimBridge({ parameters, wallet, baseNonce }: ActionProps throw new Error('[CLAIM-BRIDGE]: executeCrosschainSwap returned undefined'); } - const typedAssetToBuy = { + const typedAssetToBuy: ParsedAddressAsset = { ...parameters.assetToBuy, network: getNetworkFromChainId(parameters.assetToBuy.chainId), + chainId: parameters.assetToBuy.chainId, colors: undefined, networks: undefined, + native: undefined, }; const typedAssetToSell = { ...parameters.assetToSell, network: getNetworkFromChainId(parameters.assetToSell.chainId), + chainId: parameters.assetToSell.chainId, colors: undefined, networks: undefined, + native: undefined, }; // 5 - if the swap was successful we add the transaction to the store @@ -197,7 +201,7 @@ export async function claimBridge({ parameters, wallet, baseNonce }: ActionProps addNewTransaction({ address: bridgeQuote.from as Address, - network: getNetworkFromChainId(parameters.chainId), + chainId: parameters.chainId, transaction, }); diff --git a/src/raps/actions/crosschainSwap.ts b/src/raps/actions/crosschainSwap.ts index ac474447bc8..eeca43b96e2 100644 --- a/src/raps/actions/crosschainSwap.ts +++ b/src/raps/actions/crosschainSwap.ts @@ -1,10 +1,10 @@ import { Signer } from '@ethersproject/abstract-signer'; import { CrosschainQuote, fillCrosschainQuote } from '@rainbow-me/swaps'; import { Address } from 'viem'; -import { getProviderForNetwork, estimateGasWithPadding } from '@/handlers/web3'; +import { estimateGasWithPadding, getProvider } from '@/handlers/web3'; import { REFERRER, gasUnits, ReferrerType } from '@/references'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; import { NewTransaction } from '@/entities/transactions'; import { TxHash } from '@/resources/transactions/types'; import { addNewTransaction } from '@/state/pendingTransactions'; @@ -39,7 +39,7 @@ export const estimateCrosschainSwapGasLimit = async ({ quote: CrosschainQuote; }): Promise => { // TODO: MARK - Replace this once we migrate network => chainId - const provider = getProviderForNetwork(ethereumUtils.getNetworkFromChainId(chainId)); + const provider = getProvider({ chainId }); if (!provider || !quote) { return gasUnits.basic_swap[chainId]; } @@ -205,8 +205,10 @@ export const crosschainSwap = async ({ asset: { ...parameters.assetToSell, network: ethereumUtils.getNetworkFromChainId(parameters.assetToSell.chainId), + chainId: parameters.assetToSell.chainId, colors: parameters.assetToSell.colors as TokenColors, price: nativePriceForAssetToSell, + native: undefined, }, value: quote.sellAmount.toString(), }, @@ -217,8 +219,10 @@ export const crosschainSwap = async ({ asset: { ...parameters.assetToBuy, network: ethereumUtils.getNetworkFromChainId(parameters.assetToBuy.chainId), + chainId: parameters.assetToBuy.chainId, colors: parameters.assetToBuy.colors as TokenColors, price: nativePriceForAssetToBuy, + native: undefined, }, value: quote.buyAmountMinusFees.toString(), }, @@ -227,7 +231,6 @@ export const crosschainSwap = async ({ hash: swap.hash as TxHash, // TODO: MARK - Replace this once we migrate network => chainId network, - // chainId: parameters.chainId, nonce: swap.nonce, status: 'pending', type: 'swap', @@ -237,8 +240,7 @@ export const crosschainSwap = async ({ addNewTransaction({ address: parameters.quote.from as Address, - // chainId: parameters.chainId as ChainId, - network, + chainId, transaction, }); diff --git a/src/raps/actions/ens.ts b/src/raps/actions/ens.ts index b5a9a05a48b..6fc53450458 100644 --- a/src/raps/actions/ens.ts +++ b/src/raps/actions/ens.ts @@ -6,7 +6,6 @@ import { analytics } from '@/analytics'; import { ENSRegistrationRecords, NewTransaction, TransactionGasParamAmounts } from '@/entities'; import { estimateENSTransactionGasLimit, formatRecordsForTransaction } from '@/handlers/ens'; import { toHex } from '@/handlers/web3'; -import { NetworkTypes } from '@/helpers'; import { ENSRegistrationTransactionType, getENSExecutionDetails, REGISTRATION_MODES } from '@/helpers/ens'; import * as i18n from '@/languages'; import { saveCommitRegistrationParameters, updateTransactionRegistrationParameters } from '@/redux/ensRegistration'; @@ -14,7 +13,7 @@ import store from '@/redux/store'; import { logger, RainbowError } from '@/logger'; import { parseGasParamAmounts } from '@/parsers'; import { addNewTransaction } from '@/state/pendingTransactions'; -import { Network } from '@/networks/types'; +import { ChainId, Network } from '@/networks/types'; import { createRegisterENSRap, createRenewENSRap, @@ -25,7 +24,6 @@ import { } from '../registerENS'; import { Logger } from '@ethersproject/logger'; import { performanceTracking, Screens, TimeToSignOperation } from '@/state/performance/performance'; -import { ChainId } from '@/__swaps__/types/chains'; export interface ENSRapActionResponse { baseNonce?: number | null; @@ -481,7 +479,7 @@ const ensAction = async ( }, to: tx?.to, value: toHex(tx.value), - network: NetworkTypes.mainnet, + network: Network.mainnet, status: 'pending', }; @@ -490,7 +488,7 @@ const ensAction = async ( addNewTransaction({ address: ownerAddress, transaction: newTransaction, - network: Network.mainnet, + chainId: ChainId.mainnet, }); return tx?.nonce; }; diff --git a/src/raps/actions/swap.ts b/src/raps/actions/swap.ts index b099c686916..179f8ae7d27 100644 --- a/src/raps/actions/swap.ts +++ b/src/raps/actions/swap.ts @@ -15,11 +15,11 @@ import { unwrapNativeAsset, wrapNativeAsset, } from '@rainbow-me/swaps'; -import { getProviderForNetwork, estimateGasWithPadding } from '@/handlers/web3'; +import { estimateGasWithPadding, getProvider } from '@/handlers/web3'; import { Address } from 'viem'; import { metadataPOSTClient } from '@/graphql'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; import { NewTransaction } from '@/entities/transactions'; import { TxHash } from '@/resources/transactions/types'; import { add } from '@/helpers/utilities'; @@ -62,7 +62,7 @@ export const estimateSwapGasLimit = async ({ quote: Quote; }): Promise => { // TODO: MARK - Replace this once we migrate network => chainId - const provider = getProviderForNetwork(ethereumUtils.getNetworkFromChainId(chainId)); + const provider = getProvider({ chainId }); if (!provider || !quote) { return gasUnits.basic_swap[chainId]; } @@ -152,8 +152,7 @@ export const estimateUnlockAndSwapFromMetadata = async ({ chainId, }); - // TODO: MARK - Replace this once we migrate network => chainId - const provider = getProviderForNetwork(ethereumUtils.getNetworkFromChainId(chainId)); + const provider = getProvider({ chainId }); const swapTransaction = await populateSwap({ provider, quote, @@ -310,9 +309,6 @@ export const swap = async ({ if (!swap || !swap?.hash) throw new RainbowError('swap: error executeSwap'); - // TODO: MARK - Replace this once we migrate network => chainId - const network = ethereumUtils.getNetworkFromChainId(parameters.chainId); - const nativePriceForAssetToBuy = (parameters.assetToBuy as ExtendedAnimatedAssetWithColors)?.nativePrice ? { value: (parameters.assetToBuy as ExtendedAnimatedAssetWithColors)?.nativePrice, @@ -349,6 +345,7 @@ export const swap = async ({ network: ethereumUtils.getNetworkFromChainId(parameters.assetToSell.chainId), colors: parameters.assetToSell.colors as TokenColors, price: nativePriceForAssetToSell, + native: undefined, }, value: quote.sellAmount.toString(), }, @@ -361,6 +358,7 @@ export const swap = async ({ network: ethereumUtils.getNetworkFromChainId(parameters.assetToBuy.chainId), colors: parameters.assetToBuy.colors as TokenColors, price: nativePriceForAssetToBuy, + native: undefined, }, value: quote.buyAmountMinusFees.toString(), }, @@ -393,8 +391,7 @@ export const swap = async ({ addNewTransaction({ address: parameters.quote.from as Address, - // chainId: parameters.chainId as ChainId, - network, + chainId: parameters.chainId, transaction, }); diff --git a/src/raps/actions/unlock.ts b/src/raps/actions/unlock.ts index 5fe0898d9d6..fb31e766888 100644 --- a/src/raps/actions/unlock.ts +++ b/src/raps/actions/unlock.ts @@ -2,10 +2,10 @@ import { Signer } from '@ethersproject/abstract-signer'; import { MaxUint256 } from '@ethersproject/constants'; import { Contract, PopulatedTransaction } from '@ethersproject/contracts'; import { parseUnits } from '@ethersproject/units'; -import { getProviderForNetwork } from '@/handlers/web3'; +import { getProvider } from '@/handlers/web3'; import { Address, erc20Abi, erc721Abi } from 'viem'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; import { TransactionGasParams, TransactionLegacyGasParams } from '@/__swaps__/types/gas'; import { NewTransaction } from '@/entities/transactions'; import { TxHash } from '@/resources/transactions/types'; @@ -35,8 +35,7 @@ export const getAssetRawAllowance = async ({ chainId: ChainId; }) => { try { - // TODO: MARK - Replace this once we migrate network => chainId - const provider = getProviderForNetwork(ethereumUtils.getNetworkFromChainId(chainId)); + const provider = getProvider({ chainId }); const tokenContract = new Contract(assetAddress, erc20Abi, provider); const allowance = await tokenContract.allowance(owner, spender); return allowance.toString(); @@ -87,8 +86,7 @@ export const estimateApprove = async ({ chainId: ChainId; }): Promise => { try { - // TODO: MARK - Replace this once we migrate network => chainId - const provider = getProviderForNetwork(ethereumUtils.getNetworkFromChainId(chainId)); + const provider = getProvider({ chainId }); const tokenContract = new Contract(tokenAddress, erc20Abi, provider); const gasLimit = await tokenContract.estimateGas.approve(spender, MaxUint256, { from: owner, @@ -119,8 +117,7 @@ export const populateApprove = async ({ chainId: ChainId; }): Promise => { try { - // TODO: MARK - Replace this once we migrate network => chainId - const provider = getProviderForNetwork(ethereumUtils.getNetworkFromChainId(chainId)); + const provider = getProvider({ chainId }); const tokenContract = new Contract(tokenAddress, erc20Abi, provider); const approveTransaction = await tokenContract.populateTransaction.approve(spender, MaxUint256, { from: owner, @@ -146,8 +143,7 @@ export const estimateERC721Approval = async ({ chainId: ChainId; }): Promise => { try { - // TODO: MARK - Replace this once we migrate network => chainId - const provider = getProviderForNetwork(ethereumUtils.getNetworkFromChainId(chainId)); + const provider = getProvider({ chainId }); const tokenContract = new Contract(tokenAddress, erc721Abi, provider); const gasLimit = await tokenContract.estimateGas.setApprovalForAll(spender, false, { from: owner, @@ -173,8 +169,7 @@ export const populateRevokeApproval = async ({ type: 'erc20' | 'nft'; }): Promise => { if (!tokenAddress || !spenderAddress || !chainId) return {}; - // TODO: MARK - Replace this once we migrate network => chainId - const provider = getProviderForNetwork(ethereumUtils.getNetworkFromChainId(chainId)); + const provider = getProvider({ chainId }); const tokenContract = new Contract(tokenAddress, erc721Abi, provider); if (type === 'erc20') { const amountToApprove = parseUnits('0', 'ether'); @@ -295,12 +290,9 @@ export const unlock = async ({ ...gasParams, } satisfies NewTransaction; - // TODO: MARK - Replace this once we migrate network => chainId - const network = ethereumUtils.getNetworkFromChainId(approval.chainId); - addNewTransaction({ address: parameters.fromAddress as Address, - network, + chainId: approval.chainId, transaction, }); diff --git a/src/raps/references.ts b/src/raps/references.ts index d8f68cf8bb9..6c6bf22dba7 100644 --- a/src/raps/references.ts +++ b/src/raps/references.ts @@ -4,7 +4,7 @@ import { Address } from 'viem'; import { ParsedAsset } from '@/__swaps__/types/assets'; import { GasFeeParamsBySpeed, LegacyGasFeeParamsBySpeed, LegacyTransactionGasParamAmounts, TransactionGasParamAmounts } from '@/entities'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; export enum SwapModalField { input = 'inputAmount', diff --git a/src/raps/unlockAndSwap.ts b/src/raps/unlockAndSwap.ts index 249b8b86b27..769546efec6 100644 --- a/src/raps/unlockAndSwap.ts +++ b/src/raps/unlockAndSwap.ts @@ -7,7 +7,7 @@ import { } from '@rainbow-me/swaps'; import { Address } from 'viem'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; import { isNativeAsset } from '@/handlers/assets'; import { add } from '@/helpers/utilities'; import { isLowerCaseMatch } from '@/utils'; diff --git a/src/raps/utils.ts b/src/raps/utils.ts index b91b60cb99b..cacc8795bb2 100644 --- a/src/raps/utils.ts +++ b/src/raps/utils.ts @@ -5,11 +5,10 @@ import { StaticJsonRpcProvider } from '@ethersproject/providers'; import { ALLOWS_PERMIT, CrosschainQuote, Quote, getQuoteExecutionDetails, getRainbowRouterContractAddress } from '@rainbow-me/swaps'; import { mainnet } from 'viem/chains'; import { Chain, erc20Abi } from 'viem'; -import { Network } from '@/helpers'; import { GasFeeParamsBySpeed, LegacyGasFeeParamsBySpeed, LegacyTransactionGasParamAmounts, TransactionGasParamAmounts } from '@/entities'; import { ethereumUtils, gasUtils } from '@/utils'; import { add, greaterThan, multiply } from '@/helpers/utilities'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId, Network } from '@/networks/types'; import { gasUnits } from '@/references'; import { toHexNoLeadingZeros } from '@/handlers/web3'; diff --git a/src/redux/contacts.ts b/src/redux/contacts.ts index bfa15e0e912..7de2fe6610f 100644 --- a/src/redux/contacts.ts +++ b/src/redux/contacts.ts @@ -1,6 +1,5 @@ import { Dispatch } from 'redux'; import { getContacts, saveContacts } from '@/handlers/localstorage/contacts'; -import { Network } from '@/helpers/networkTypes'; import { omitFlatten } from '@/helpers/utilities'; import { AppGetState } from '@/redux/store'; import { handleReviewPromptAction } from '@/utils/reviewAlert'; @@ -33,11 +32,6 @@ export interface Contact { */ ens: string; - /** - * The network. - */ - network: Network; - /** * The contact's nickname. */ @@ -85,8 +79,7 @@ export const contactsLoadState = () => async (dispatch: Dispatch - (dispatch: Dispatch, getState: AppGetState) => { + (address: string, nickname: string, color: number, ens: string) => (dispatch: Dispatch, getState: AppGetState) => { const loweredAddress = address.toLowerCase(); const { contacts } = getState().contacts; const updatedContacts = { @@ -95,7 +88,6 @@ export const contactsAddOrUpdate = address: loweredAddress, color, ens, - network, nickname, }, }; diff --git a/src/redux/ensRegistration.ts b/src/redux/ensRegistration.ts index 69d95299692..44c6d1041d4 100644 --- a/src/redux/ensRegistration.ts +++ b/src/redux/ensRegistration.ts @@ -3,9 +3,9 @@ import { Dispatch } from 'react'; import { AppDispatch, AppGetState } from './store'; import { ENSRegistrations, ENSRegistrationState, Records, RegistrationParameters, TransactionRegistrationParameters } from '@/entities'; import { getLocalENSRegistrations, saveLocalENSRegistrations } from '@/handlers/localstorage/accountLocal'; -import { NetworkTypes } from '@/helpers'; import { ENS_RECORDS, REGISTRATION_MODES } from '@/helpers/ens'; import { omitFlatten } from '@/helpers/utilities'; +import { Network } from '@/networks/types'; const ENS_REGISTRATION_SET_CHANGED_RECORDS = 'ensRegistration/ENS_REGISTRATION_SET_CHANGED_RECORDS'; const ENS_REGISTRATION_SET_INITIAL_RECORDS = 'ensRegistration/ENS_REGISTRATION_SET_INITIAL_RECORDS'; @@ -345,7 +345,7 @@ export const saveCommitRegistrationParameters = }, }; - saveLocalENSRegistrations(updatedEnsRegistrationManager.registrations, accountAddress, NetworkTypes.mainnet); + saveLocalENSRegistrations(updatedEnsRegistrationManager.registrations, accountAddress, Network.mainnet); dispatch({ payload: updatedEnsRegistrationManager, @@ -382,7 +382,7 @@ export const updateTransactionRegistrationParameters = }, }; - saveLocalENSRegistrations(updatedEnsRegistrationManager.registrations, accountAddress, NetworkTypes.mainnet); + saveLocalENSRegistrations(updatedEnsRegistrationManager.registrations, accountAddress, Network.mainnet); dispatch({ payload: updatedEnsRegistrationManager, @@ -408,7 +408,7 @@ export const removeRegistrationByName = (name: string) => async (dispatch: AppDi }, }; - saveLocalENSRegistrations(updatedEnsRegistrationManager.registrations, accountAddress, NetworkTypes.mainnet); + saveLocalENSRegistrations(updatedEnsRegistrationManager.registrations, accountAddress, Network.mainnet); dispatch({ payload: updatedEnsRegistrationManager, diff --git a/src/redux/explorer.ts b/src/redux/explorer.ts index 20c9f151f31..0892e4b5e8c 100644 --- a/src/redux/explorer.ts +++ b/src/redux/explorer.ts @@ -4,8 +4,8 @@ import { ThunkDispatch } from 'redux-thunk'; import { io, Socket } from 'socket.io-client'; import { getRemoteConfig } from '@/model/remoteConfig'; import { AppGetState, AppState } from './store'; -import { getProviderForNetwork, isHardHat } from '@/handlers/web3'; -import { Network } from '@/helpers/networkTypes'; +import { ChainId } from '@/networks/types'; +import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; // -- Constants --------------------------------------- // const EXPLORER_UPDATE_SOCKETS = 'explorer/EXPLORER_UPDATE_SOCKETS'; @@ -110,7 +110,7 @@ export const explorerClearState = () => (dispatch: ThunkDispatch (dispatch: ThunkDispatch, getState: AppGetState) => { - const { network, accountAddress } = getState().settings; + const { accountAddress, chainId } = getState().settings; const { addressSocket } = getState().explorer; // if there is another socket unsubscribe first @@ -118,9 +118,7 @@ export const explorerInit = () => (dispatch: ThunkDispatch void) => await mutex.runExclusive(callback); -const getGasPricePollingInterval = (network: Network): number => { - return getNetworkObj(network).gas.pollingIntervalInMs; +const getGasPricePollingInterval = (chainId: ChainId): number => { + return getNetworkObject({ chainId }).gas.pollingIntervalInMs; }; -const getDefaultGasLimit = (network: Network, defaultGasLimit: number): number => { - switch (network) { - case Network.arbitrum: +const getDefaultGasLimit = (chainId: ChainId, defaultGasLimit: number): number => { + switch (chainId) { + case ChainId.arbitrum: return ethUnits.arbitrum_basic_tx; - case Network.polygon: - case Network.bsc: - case Network.optimism: - case Network.mainnet: - case Network.zora: + case ChainId.polygon: + case ChainId.bsc: + case ChainId.optimism: + case ChainId.mainnet: + case ChainId.zora: default: return defaultGasLimit; } @@ -73,7 +74,7 @@ export interface GasState { gasFeeParamsBySpeed: GasFeeParamsBySpeed | LegacyGasFeeParamsBySpeed; selectedGasFee: SelectedGasFee | LegacySelectedGasFee; gasFeesBySpeed: GasFeesBySpeed | LegacyGasFeesBySpeed; - txNetwork: Network | null; + chainId: ChainId; currentBlockParams: CurrentBlockParams; blocksToConfirmation: BlocksToConfirmation; customGasFeeModifiedByUser: boolean; @@ -121,22 +122,22 @@ const getUpdatedGasFeeParams = ( gasLimit: string, nativeCurrency: NativeCurrencyKey, selectedGasFeeOption: string, - txNetwork: Network, + chainId: ChainId, l1GasFeeOptimism: BigNumber | null = null ) => { let nativeTokenPriceUnit = ethereumUtils.getEthPriceUnit(); - switch (txNetwork) { - case Network.polygon: + switch (chainId) { + case ChainId.polygon: nativeTokenPriceUnit = ethereumUtils.getMaticPriceUnit(); break; - case Network.bsc: + case ChainId.bsc: nativeTokenPriceUnit = ethereumUtils.getBnbPriceUnit(); break; - case Network.avalanche: + case ChainId.avalanche: nativeTokenPriceUnit = ethereumUtils.getAvaxPriceUnit(); break; - case Network.degen: + case ChainId.degen: nativeTokenPriceUnit = ethereumUtils.getDegenPriceUnit(); break; default: @@ -144,7 +145,7 @@ const getUpdatedGasFeeParams = ( break; } - const isLegacyGasNetwork = getNetworkObj(txNetwork).gas.gasType === 'legacy'; + const isLegacyGasNetwork = getNetworkObject({ chainId }).gas.gasType === 'legacy'; const gasFeesBySpeed = isLegacyGasNetwork ? parseLegacyGasFeesBySpeed( @@ -192,7 +193,7 @@ export const updateGasFeeForSpeed = export const gasUpdateToCustomGasFee = (gasParams: GasFeeParams) => async (dispatch: AppDispatch, getState: AppGetState) => { const { - txNetwork, + chainId, defaultGasLimit, gasFeesBySpeed, gasFeeParamsBySpeed, @@ -204,15 +205,15 @@ export const gasUpdateToCustomGasFee = (gasParams: GasFeeParams) => async (dispa } = getState().gas; const { nativeCurrency } = getState().settings; - const _gasLimit = gasLimit || getDefaultGasLimit(txNetwork, defaultGasLimit); + const _gasLimit = gasLimit || getDefaultGasLimit(chainId, defaultGasLimit); let nativeTokenPriceUnit = ethereumUtils.getEthPriceUnit(); - switch (txNetwork) { - case Network.polygon: + switch (chainId) { + case ChainId.polygon: nativeTokenPriceUnit = ethereumUtils.getMaticPriceUnit(); break; - case Network.bsc: + case ChainId.bsc: nativeTokenPriceUnit = ethereumUtils.getBnbPriceUnit(); break; default: @@ -257,7 +258,7 @@ export const getPolygonGasPrices = async () => { data: { data: { legacy: result }, }, - } = (await rainbowMeteorologyGetData(Network.polygon)) as { + } = (await rainbowMeteorologyGetData(ChainId.polygon)) as { data: RainbowMeteorologyLegacyData; }; const polygonGasPriceBumpFactor = 1.05; @@ -287,7 +288,7 @@ export const getBscGasPrices = async () => { data: { data: { legacy: result }, }, - } = (await rainbowMeteorologyGetData(Network.bsc)) as { + } = (await rainbowMeteorologyGetData(ChainId.bsc)) as { data: RainbowMeteorologyLegacyData; }; @@ -312,7 +313,7 @@ export const getBscGasPrices = async () => { } }; export const getArbitrumGasPrices = async () => { - const provider = getProviderForNetwork(Network.arbitrum); + const provider = getProvider({ chainId: ChainId.arbitrum }); const baseGasPrice = await provider.getGasPrice(); const normalGasPrice = weiToGwei(baseGasPrice.toString()); @@ -330,7 +331,7 @@ export const getArbitrumGasPrices = async () => { }; export const getOptimismGasPrices = async () => { - const provider = getProviderForNetwork(Network.optimism); + const provider = getProvider({ chainId: ChainId.optimism }); const baseGasPrice = await provider.getGasPrice(); const normalGasPrice = weiToGwei(baseGasPrice.toString()); @@ -347,7 +348,7 @@ export const getOptimismGasPrices = async () => { }; export const getBaseGasPrices = async () => { - const provider = getProviderForNetwork(Network.base); + const provider = getProvider({ chainId: ChainId.base }); const baseGasPrice = await provider.getGasPrice(); const BasePriceBumpFactor = 1.05; @@ -366,7 +367,7 @@ export const getBaseGasPrices = async () => { }; export const getAvalancheGasPrices = async () => { - const provider = getProviderForNetwork(Network.avalanche); + const provider = getProvider({ chainId: ChainId.avalanche }); const baseGasPrice = await provider.getGasPrice(); const AvalanchePriceBumpFactor = 1.05; @@ -385,7 +386,7 @@ export const getAvalancheGasPrices = async () => { }; export const getDegenGasPrices = async () => { - const provider = getProviderForNetwork(Network.degen); + const provider = getProvider({ chainId: ChainId.degen }); const baseGasPrice = await provider.getGasPrice(); const DegenPriceBumpFactor = 1.05; @@ -404,7 +405,7 @@ export const getDegenGasPrices = async () => { }; export const getBlastGasPrices = async () => { - const provider = getProviderForNetwork(Network.blast); + const provider = getProvider({ chainId: ChainId.blast }); const baseGasPrice = await provider.getGasPrice(); const BlastPriceBumpFactor = 1.05; @@ -423,7 +424,7 @@ export const getBlastGasPrices = async () => { }; export const getZoraGasPrices = async () => { - const provider = getProviderForNetwork(Network.zora); + const provider = getProvider({ chainId: ChainId.zora }); const baseGasPrice = await provider.getGasPrice(); const ZoraPriceBumpFactor = 1.05; @@ -441,12 +442,12 @@ export const getZoraGasPrices = async () => { return priceData; }; -export const getEIP1559GasParams = async (network: Network) => { - const { data } = (await rainbowMeteorologyGetData(network)) as { +export const getEIP1559GasParams = async (chainId: ChainId) => { + const { data } = (await rainbowMeteorologyGetData(chainId)) as { data: RainbowMeteorologyData; }; const { gasFeeParamsBySpeed, baseFeePerGas, baseFeeTrend, currentBaseFee, blocksToConfirmation, secondsPerNewBlock } = - parseRainbowMeteorologyData(data, network); + parseRainbowMeteorologyData(data); return { baseFeePerGas, blocksToConfirmation, @@ -458,44 +459,45 @@ export const getEIP1559GasParams = async (network: Network) => { }; export const gasPricesStartPolling = - (network = Network.mainnet, flashbots = false) => + (chainId = ChainId.mainnet, flashbots = false) => async (dispatch: AppDispatch, getState: AppGetState) => { dispatch(gasPricesStopPolling()); // this should be chain agnostic - const getGasPrices = (network: Network) => + const getGasPrices = () => withRunExclusive( () => new Promise(async (fetchResolve, fetchReject) => { try { - dispatch({ - payload: network, - type: GAS_UPDATE_TRANSACTION_NETWORK, - }); const { gasFeeParamsBySpeed: existingGasFees, customGasFeeModifiedByUser, defaultGasLimit, gasLimit, selectedGasFee, - txNetwork, + chainId, selectedGasFee: lastSelectedGasFee, gasFeesBySpeed: lastGasFeesBySpeed, currentBlockParams, l1GasFeeOptimism, } = getState().gas; + dispatch({ + payload: chainId, + type: GAS_UPDATE_TRANSACTION_NETWORK, + }); + const { nativeCurrency } = getState().settings; - const networkObj = getNetworkObj(network); + const networkObject = getNetworkObject({ chainId }); let dataIsReady = true; - if (networkObj.gas.gasType === 'legacy') { + if (networkObject.gas.gasType === 'legacy') { // OP chains have an additional fee we need to load - if (getNetworkObj(network).gas?.OptimismTxFee) { + if (networkObject.gas?.OptimismTxFee) { dataIsReady = l1GasFeeOptimism !== null; } - const adjustedGasFees = await networkObj.gas.getGasPrices(); + const adjustedGasFees = await networkObject.gas.getGasPrices(); if (!adjustedGasFees) return; @@ -503,7 +505,7 @@ export const gasPricesStartPolling = if (!gasFeeParamsBySpeed) return; const _selectedGasFeeOption = selectedGasFee.option || NORMAL; - const _gasLimit = gasLimit || getDefaultGasLimit(txNetwork, defaultGasLimit); + const _gasLimit = gasLimit || getDefaultGasLimit(chainId, defaultGasLimit); const { selectedGasFee: updatedSelectedGasFee, gasFeesBySpeed: updatedGasFeesBySpeed } = dataIsReady ? getUpdatedGasFeeParams( currentBlockParams?.baseFeePerGas, @@ -511,7 +513,7 @@ export const gasPricesStartPolling = _gasLimit, nativeCurrency, _selectedGasFeeOption, - txNetwork, + chainId, l1GasFeeOptimism ) : { @@ -529,7 +531,7 @@ export const gasPricesStartPolling = } else { try { const { gasFeeParamsBySpeed, baseFeePerGas, trend, currentBaseFee, blocksToConfirmation, secondsPerNewBlock } = - await getEIP1559GasParams(network); + await getEIP1559GasParams(chainId); if (flashbots) { [SLOW, NORMAL, FAST, URGENT].forEach(speed => { @@ -553,16 +555,8 @@ export const gasPricesStartPolling = // Set a really gas estimate to guarantee that we're gonna be over // the basefee at the time we fork mainnet during our hardhat tests let baseFee = baseFeePerGas; - if (network === Network.mainnet && IS_TESTING === 'true') { - const providerUrl = ( - web3Provider || - ({} as { - connection: { url: string }; - }) - )?.connection?.url; - if (isHardHat(providerUrl)) { - baseFee = parseGasFeeParam(gweiToWei(1000)); - } + if (chainId === ChainId.mainnet && IS_TESTING === 'true' && useConnectedToHardhatStore.getState().connectedToHardhat) { + baseFee = parseGasFeeParam(gweiToWei(1000)); } if (customGasFeeModifiedByUser) { @@ -576,7 +570,7 @@ export const gasPricesStartPolling = gasFeeParamsBySpeed[CUSTOM] = gasFeeParamsBySpeed[URGENT]; } const _selectedGasFeeOption = selectedGasFee.option || NORMAL; - const _gasLimit = gasLimit || getDefaultGasLimit(txNetwork, defaultGasLimit); + const _gasLimit = gasLimit || getDefaultGasLimit(chainId, defaultGasLimit); const { selectedGasFee: updatedSelectedGasFee, gasFeesBySpeed } = getUpdatedGasFeeParams( currentBaseFee, @@ -584,7 +578,7 @@ export const gasPricesStartPolling = _gasLimit, nativeCurrency, _selectedGasFeeOption, - txNetwork, + chainId, l1GasFeeOptimism ); @@ -608,27 +602,27 @@ export const gasPricesStartPolling = } fetchResolve(true); } catch (e) { - logger.error(new RainbowError(`[redux/gas]: Gas Estimates Failed for ${network}: ${e}`)); + logger.error(new RainbowError(`[redux/gas]: Gas Estimates Failed for ${chainId}: ${e}`)); fetchReject(e); } }) ); - const watchGasPrices = async (network: Network, pollingInterval: number) => { + const watchGasPrices = async (chainId: ChainId, pollingInterval: number) => { try { - await getGasPrices(network); + await getGasPrices(); // eslint-disable-next-line no-empty } catch (e) { } finally { gasPricesHandle && clearTimeout(gasPricesHandle); gasPricesHandle = setTimeout(() => { - watchGasPrices(network, pollingInterval); + watchGasPrices(chainId, pollingInterval); }, pollingInterval); } }; - const pollingInterval = getGasPricePollingInterval(network); - watchGasPrices(network, pollingInterval); + const pollingInterval = getGasPricePollingInterval(chainId); + watchGasPrices(chainId, pollingInterval); }; export const gasUpdateGasFeeOption = (newGasPriceOption: string) => (dispatch: AppDispatch, getState: AppGetState) => @@ -660,10 +654,10 @@ export const gasUpdateTxFee = (updatedGasLimit?: number, overrideGasOption?: string, l1GasFeeOptimism: BigNumber | null = null) => (dispatch: AppDispatch, getState: AppGetState) => withRunExclusive(async () => { - const { defaultGasLimit, gasLimit, gasFeeParamsBySpeed, selectedGasFee, txNetwork, currentBlockParams } = getState().gas; + const { defaultGasLimit, gasLimit, gasFeeParamsBySpeed, selectedGasFee, chainId, currentBlockParams } = getState().gas; const { nativeCurrency } = getState().settings; - if (isEmpty(gasFeeParamsBySpeed) || (getNetworkObj(txNetwork).gas?.OptimismTxFee && l1GasFeeOptimism === null)) { + if (isEmpty(gasFeeParamsBySpeed) || (getNetworkObject({ chainId }).gas?.OptimismTxFee && l1GasFeeOptimism === null)) { // if fee prices not ready, we need to store the gas limit for future calculations // the rest is as the initial state value if (updatedGasLimit) { @@ -674,7 +668,7 @@ export const gasUpdateTxFee = } } else { const _selectedGasFeeOption = overrideGasOption || selectedGasFee.option || NORMAL; - const _gasLimit = updatedGasLimit || gasLimit || getDefaultGasLimit(txNetwork, defaultGasLimit); + const _gasLimit = updatedGasLimit || gasLimit || getDefaultGasLimit(chainId, defaultGasLimit); const { selectedGasFee: updatedSelectedGasFee, gasFeesBySpeed } = getUpdatedGasFeeParams( currentBlockParams?.baseFeePerGas, @@ -682,7 +676,7 @@ export const gasUpdateTxFee = _gasLimit, nativeCurrency, _selectedGasFeeOption, - txNetwork, + chainId, l1GasFeeOptimism ); dispatch({ @@ -715,7 +709,7 @@ const INITIAL_STATE: GasState = { gasLimit: null, l1GasFeeOptimism: null, selectedGasFee: {} as SelectedGasFee, - txNetwork: null, + chainId: ChainId.mainnet, secondsPerNewBlock: 15, }; @@ -770,7 +764,7 @@ export default (state = INITIAL_STATE, action: { type: string; payload: any }) = case GAS_UPDATE_TRANSACTION_NETWORK: return { ...state, - txNetwork: action.payload, + chainId: action.payload, }; case GAS_PRICES_RESET: return INITIAL_STATE; diff --git a/src/redux/requests.ts b/src/redux/requests.ts index aa5196b9cd4..7733af73291 100644 --- a/src/redux/requests.ts +++ b/src/redux/requests.ts @@ -6,7 +6,7 @@ import { getLocalRequests, removeLocalRequest, saveLocalRequests } from '@/handl import { omitFlatten } from '@/helpers/utilities'; import { getRequestDisplayDetails } from '@/parsers'; import { logger } from '@/logger'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; // -- Constants --------------------------------------- // @@ -71,7 +71,7 @@ export interface WalletconnectRequestData extends RequestData { /** * Display details loaded for a request. */ -interface RequestDisplayDetails { +export interface RequestDisplayDetails { /** * Data loaded for the request, depending on the type of request. */ @@ -154,7 +154,7 @@ export const addRequestToApprove = icons?: string[]; } ) => - (dispatch: Dispatch, getState: AppGetState) => { + async (dispatch: Dispatch, getState: AppGetState) => { const { requests } = getState().requests; const { walletConnectors } = getState().walletconnect; const { accountAddress, network, nativeCurrency } = getState().settings; @@ -163,7 +163,7 @@ export const addRequestToApprove = const chainId = walletConnector._chainId; // @ts-expect-error "_accounts" is private. const address = walletConnector._accounts[0]; - const displayDetails = getRequestDisplayDetails(payload, nativeCurrency, chainId); + const displayDetails = await getRequestDisplayDetails(payload, nativeCurrency, chainId); const oneHourAgoTs = Date.now() - EXPIRATION_THRESHOLD_IN_MS; // @ts-expect-error This fails to compile as `displayDetails` does not // always return an object with `timestampInMs`. Still, the error thrown diff --git a/src/redux/settings.ts b/src/redux/settings.ts index 728caf45bcd..844d28b1348 100644 --- a/src/redux/settings.ts +++ b/src/redux/settings.ts @@ -10,24 +10,23 @@ import { WrappedAlert as Alert } from '@/helpers/alert'; import { getAppIcon, + getChainId, getFlashbotsEnabled, getLanguage, getNativeCurrency, - getNetwork, getTestnetsEnabled, saveAppIcon, + saveChainId, saveFlashbotsEnabled, saveLanguage, saveNativeCurrency, - saveNetwork, saveTestnetsEnabled, } from '@/handlers/localstorage/globalSettings'; import { web3SetHttpProvider } from '@/handlers/web3'; -import { Network } from '@/helpers/networkTypes'; import { explorerClearState, explorerInit } from '@/redux/explorer'; import { AppState } from '@/redux/store'; -import { ethereumUtils } from '@/utils'; import { logger, RainbowError } from '@/logger'; +import { Network, ChainId } from '@/networks/types'; // -- Constants ------------------------------------------------------------- // const SETTINGS_UPDATE_SETTINGS_ADDRESS = 'settings/SETTINGS_UPDATE_SETTINGS_ADDRESS'; @@ -105,7 +104,6 @@ interface SettingsStateUpdateNetworkSuccessAction { type: typeof SETTINGS_UPDATE_NETWORK_SUCCESS; payload: { chainId: SettingsState['chainId']; - network: SettingsState['network']; }; } @@ -151,11 +149,10 @@ export const settingsLoadState = export const settingsLoadNetwork = () => async (dispatch: Dispatch) => { try { - const network = await getNetwork(); - const chainId = ethereumUtils.getChainIdFromNetwork(network); - await web3SetHttpProvider(network); + const chainId = await getChainId(); + await web3SetHttpProvider(chainId); dispatch({ - payload: { chainId, network }, + payload: { chainId }, type: SETTINGS_UPDATE_NETWORK_SUCCESS, }); } catch (error) { @@ -237,15 +234,14 @@ export const settingsUpdateAccountAddress = }); }; -export const settingsUpdateNetwork = (network: Network) => async (dispatch: Dispatch) => { - const chainId = ethereumUtils.getChainIdFromNetwork(network); - await web3SetHttpProvider(network); +export const settingsUpdateNetwork = (chainId: ChainId) => async (dispatch: Dispatch) => { + await web3SetHttpProvider(chainId); try { dispatch({ - payload: { chainId, network }, + payload: { chainId }, type: SETTINGS_UPDATE_NETWORK_SUCCESS, }); - saveNetwork(network); + saveChainId(chainId); } catch (error) { logger.error(new RainbowError(`[redux/settings]: Error updating network settings: ${error}`)); } @@ -315,7 +311,6 @@ export default (state = INITIAL_STATE, action: SettingsStateUpdateAction) => { return { ...state, chainId: action.payload.chainId, - network: action.payload.network, }; case SETTINGS_UPDATE_LANGUAGE_SUCCESS: return { diff --git a/src/redux/showcaseTokens.ts b/src/redux/showcaseTokens.ts index d0d1d934ab5..23252088f9f 100644 --- a/src/redux/showcaseTokens.ts +++ b/src/redux/showcaseTokens.ts @@ -4,8 +4,8 @@ import { Dispatch } from 'redux'; import { getPreference } from '../model/preferences'; import { AppGetState } from './store'; import { getShowcaseTokens, getWebDataEnabled, saveShowcaseTokens, saveWebDataEnabled } from '@/handlers/localstorage/accountLocal'; -import networkTypes from '@/helpers/networkTypes'; import WalletTypes from '@/helpers/walletTypes'; +import { Network } from '@/networks/types'; // -- Constants --------------------------------------- // @@ -204,7 +204,7 @@ export const removeShowcaseToken = (tokenId: string) => (dispatch: Dispatch + (enabled: boolean, address: string, network = Network.mainnet) => async (dispatch: Dispatch) => { dispatch({ payload: enabled, diff --git a/src/redux/swap.ts b/src/redux/swap.ts index 10bf5a81080..0ca1da8addd 100644 --- a/src/redux/swap.ts +++ b/src/redux/swap.ts @@ -109,7 +109,7 @@ export const updateSwapInputCurrency = dispatch({ payload: newInputCurrency, type: SWAP_UPDATE_INPUT_CURRENCY }); if ( type === ExchangeModalTypes.swap && - newInputCurrency?.network !== outputCurrency?.network && + newInputCurrency?.chainId !== outputCurrency?.chainId && newInputCurrency && !ignoreTypeCheck ) { @@ -131,7 +131,7 @@ export const updateSwapOutputCurrency = } else { if ( type === ExchangeModalTypes.swap && - newOutputCurrency?.network !== inputCurrency?.network && + newOutputCurrency?.chainId !== inputCurrency?.chainId && newOutputCurrency && !ignoreTypeCheck ) { diff --git a/src/redux/walletconnect.ts b/src/redux/walletconnect.ts index da0ea48400c..c4658b7e090 100644 --- a/src/redux/walletconnect.ts +++ b/src/redux/walletconnect.ts @@ -25,13 +25,15 @@ import { findWalletWithAccount } from '@/helpers/findWalletWithAccount'; import { convertHexToString, delay, omitBy, pickBy } from '@/helpers/utilities'; import WalletConnectApprovalSheetType from '@/helpers/walletConnectApprovalSheetTypes'; import Routes from '@/navigation/routesNames'; -import { ethereumUtils, watchingAlert } from '@/utils'; +import { watchingAlert } from '@/utils'; import { getFCMToken } from '@/notifications/tokens'; import { logger, RainbowError } from '@/logger'; import { IS_DEV, IS_IOS, IS_TEST } from '@/env'; -import { RainbowNetworks } from '@/networks'; +import { RainbowNetworkObjects } from '@/networks'; import { Verify } from '@walletconnect/types'; import { RequestSource, handleWalletConnectRequest } from '@/utils/requestNavigationHandlers'; +import { ChainId } from '@/networks/types'; +import { Address } from 'viem'; // -- Variables --------------------------------------- // let showRedirectSheetThreshold = 300; @@ -132,8 +134,8 @@ export type WalletconnectResultType = 'timedOut' | 'sign' | 'transaction' | 'sig export interface WalletconnectApprovalSheetRouteParams { callback: ( approved: boolean, - chainId: number, - accountAddress: string, + chainId: ChainId, + accountAddress: Address, peerId: WalletconnectRequestData['peerId'], dappScheme: WalletconnectRequestData['dappScheme'], dappName: WalletconnectRequestData['dappName'], @@ -470,11 +472,11 @@ const listenOnNewMessages = const requestId = payload.id; if (payload.method === 'wallet_addEthereumChain' || payload.method === `wallet_switchEthereumChain`) { const { chainId } = payload.params[0]; - const currentNetwork = ethereumUtils.getNetworkFromChainId( - // @ts-expect-error "_chainId" is private. - Number(walletConnector._chainId) + // @ts-expect-error "_chainId" is private. + const currentChainId = Number(walletConnector._chainId); + const supportedChains = RainbowNetworkObjects.filter(network => network.features.walletconnect).map(network => + network.id.toString() ); - const supportedChains = RainbowNetworks.filter(network => network.features.walletconnect).map(network => network.id.toString()); const numericChainId = convertHexToString(chainId); if (supportedChains.includes(numericChainId)) { dispatch(walletConnectSetPendingRedirect()); @@ -511,7 +513,7 @@ const listenOnNewMessages = }); } }, - currentNetwork, + currentChainId, meta: { chainIds: [Number(numericChainId)], dappName, @@ -559,7 +561,9 @@ const listenOnNewMessages = return; } const { requests: pendingRequests } = getState().requests; - const request = !pendingRequests[requestId] ? dispatch(addRequestToApprove(clientId, peerId, requestId, payload, peerMeta)) : null; + const request = !pendingRequests[requestId] + ? await dispatch(addRequestToApprove(clientId, peerId, requestId, payload, peerMeta)) + : null; if (request) { handleWalletConnectRequest(request); InteractionManager.runAfterInteractions(() => { diff --git a/src/references/chain-assets.json b/src/references/chain-assets.json index 766268a1054..3f05d7c252a 100644 --- a/src/references/chain-assets.json +++ b/src/references/chain-assets.json @@ -1,7 +1,8 @@ { - "goerli": [ + "5": [ { "asset": { + "chainId": 5, "asset_code": "eth", "mainnet_address": "eth", "colors": { @@ -29,9 +30,10 @@ "quantity": "0" } ], - "mainnet": [ + "1": [ { "asset": { + "chainId": 1, "asset_code": "eth", "colors": { "fallback": "#E8EAF5", diff --git a/src/references/gasUnits.ts b/src/references/gasUnits.ts index d92d4d42df2..02e494dcdf6 100644 --- a/src/references/gasUnits.ts +++ b/src/references/gasUnits.ts @@ -1,4 +1,4 @@ -import { ChainId } from '../__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; export const gasUnits = { basic_approval: '55000', diff --git a/src/references/index.ts b/src/references/index.ts index a692c06120e..5d4e5caec68 100644 --- a/src/references/index.ts +++ b/src/references/index.ts @@ -1,7 +1,6 @@ import { AddressOrEth } from '@/__swaps__/types/assets'; -import { ChainId, ChainNameDisplay } from '@/__swaps__/types/chains'; +import { ChainId, ChainNameDisplay, Network } from '@/networks/types'; import { Asset } from '@/entities'; -import { Network } from '@/helpers/networkTypes'; import { AddressZero } from '@ethersproject/constants'; import type { Address } from 'viem'; diff --git a/src/references/rainbow-token-list/index.ts b/src/references/rainbow-token-list/index.ts index 29a59976443..688e07eef13 100644 --- a/src/references/rainbow-token-list/index.ts +++ b/src/references/rainbow-token-list/index.ts @@ -6,8 +6,7 @@ import RAINBOW_TOKEN_LIST_DATA from './rainbow-token-list.json'; import { RainbowToken } from '@/entities'; import { STORAGE_IDS } from '@/model/mmkv'; import { logger, RainbowError } from '@/logger'; -import { Network } from '@/networks/types'; -import { ChainId } from '@/__swaps__/types/chains'; +import { Network, ChainId } from '@/networks/types'; export const rainbowListStorage = new MMKV({ id: STORAGE_IDS.RAINBOW_TOKEN_LIST, diff --git a/src/references/testnet-assets-by-chain.ts b/src/references/testnet-assets-by-chain.ts index eb56ab9f147..8bfb3c1d61f 100644 --- a/src/references/testnet-assets-by-chain.ts +++ b/src/references/testnet-assets-by-chain.ts @@ -1,6 +1,5 @@ import { UniqueId, ZerionAsset } from '@/__swaps__/types/assets'; -import { ChainName } from '@/__swaps__/types/chains'; -import { Network } from '@/helpers'; +import { ChainId, ChainName } from '@/networks/types'; type ChainAssets = { [uniqueId: UniqueId]: { @@ -10,8 +9,8 @@ type ChainAssets = { }; // NOTE: Don't import `ETH_ADDRESS` as it's resolving to undefined... -export const chainAssets: Partial> = { - [Network.goerli]: { +export const chainAssets: Partial> = { + [ChainId.goerli]: { eth_5: { asset: { asset_code: 'eth', @@ -38,7 +37,7 @@ export const chainAssets: Partial> = { quantity: '0', }, }, - [Network.mainnet]: { + [ChainId.mainnet]: { eth_1: { asset: { asset_code: 'eth', diff --git a/src/resources/assets/UserAssetsQuery.ts b/src/resources/assets/UserAssetsQuery.ts index 5f2ea008d0e..47db136d5e2 100644 --- a/src/resources/assets/UserAssetsQuery.ts +++ b/src/resources/assets/UserAssetsQuery.ts @@ -2,15 +2,16 @@ import isEmpty from 'lodash/isEmpty'; import { ADDYS_API_KEY } from 'react-native-dotenv'; import { NativeCurrencyKey } from '@/entities'; import { saveAccountEmptyState } from '@/handlers/localstorage/accountLocal'; -import { Network } from '@/helpers/networkTypes'; import { greaterThan } from '@/helpers/utilities'; -import { RainbowNetworks } from '@/networks'; +import { RainbowNetworkObjects } from '@/networks'; import { rainbowFetch } from '@/rainbow-fetch'; import { createQueryKey, queryClient, QueryConfigWithSelect, QueryFunctionArgs, QueryFunctionResult } from '@/react-query'; import { useQuery } from '@tanstack/react-query'; import { filterPositionsData, parseAddressAsset } from './assets'; import { fetchHardhatBalances } from './hardhatAssets'; import { AddysAccountAssetsMeta, AddysAccountAssetsResponse, RainbowAddressAssets } from './types'; +import { Network } from '@/networks/types'; +import { staleBalancesStore } from '@/state/staleBalances'; // /////////////////////////////////////////////// // Query Types @@ -32,15 +33,26 @@ type UserAssetsQueryKey = ReturnType; // /////////////////////////////////////////////// // Query Function -const fetchUserAssetsForChainIds = async (address: string, currency: NativeCurrencyKey, chainIds: number[]) => { +const fetchUserAssetsForChainIds = async ({ + address, + currency, + chainIds, + staleBalanceParam, +}: { + address: string; + currency: NativeCurrencyKey; + chainIds: number[]; + staleBalanceParam?: string; +}) => { const chainIdsString = chainIds.join(','); - const url = `https://addys.p.rainbow.me/v3/${chainIdsString}/${address}/assets`; + let url = `https://addys.p.rainbow.me/v3/${chainIdsString}/${address}/assets?currency=${currency.toLowerCase()}`; + + if (staleBalanceParam) { + url += url + staleBalanceParam; + } const response = await rainbowFetch(url, { method: 'get', - params: { - currency: currency.toLowerCase(), - }, headers: { Authorization: `Bearer ${ADDYS_API_KEY}`, }, @@ -62,9 +74,14 @@ async function userAssetsQueryFunction({ } try { - const chainIds = RainbowNetworks.filter(network => network.enabled && network.networkType !== 'testnet').map(network => network.id); + const chainIds = RainbowNetworkObjects.filter(network => network.enabled && network.networkType !== 'testnet').map( + network => network.id + ); - const { erroredChainIds, results } = await fetchAndParseUserAssetsForChainIds(address, currency, chainIds); + staleBalancesStore.getState().clearExpiredData(address); + const staleBalanceParam = staleBalancesStore.getState().getStaleBalancesQueryParam(address); + + const { erroredChainIds, results } = await fetchAndParseUserAssetsForChainIds({ address, currency, chainIds, staleBalanceParam }); let parsedSuccessResults = results; // grab cached data for chain IDs with errors @@ -100,7 +117,7 @@ const retryErroredChainIds = async ( connectedToHardhat: boolean, erroredChainIds: number[] ) => { - const { meta, results } = await fetchAndParseUserAssetsForChainIds(address, currency, erroredChainIds); + const { meta, results } = await fetchAndParseUserAssetsForChainIds({ address, currency, chainIds: erroredChainIds }); let parsedSuccessResults = results; const successChainIds = meta?.chain_ids; @@ -140,12 +157,18 @@ interface AssetsAndMetadata { results: RainbowAddressAssets; } -const fetchAndParseUserAssetsForChainIds = async ( - address: string, - currency: NativeCurrencyKey, - chainIds: number[] -): Promise => { - const data = await fetchUserAssetsForChainIds(address, currency, chainIds); +const fetchAndParseUserAssetsForChainIds = async ({ + address, + currency, + chainIds, + staleBalanceParam, +}: { + address: string; + currency: NativeCurrencyKey; + chainIds: number[]; + staleBalanceParam?: string; +}): Promise => { + const data = await fetchUserAssetsForChainIds({ address, currency, chainIds, staleBalanceParam }); let parsedSuccessResults = parseUserAssetsByChain(data); // filter out positions data diff --git a/src/resources/assets/assetSelectors.ts b/src/resources/assets/assetSelectors.ts index c5fb9bf467b..41d6d2ba9e5 100644 --- a/src/resources/assets/assetSelectors.ts +++ b/src/resources/assets/assetSelectors.ts @@ -1,4 +1,4 @@ -import { ParsedAddressAsset } from '@/entities'; +import { NativeCurrencyKey, ParsedAddressAsset } from '@/entities'; import { parseAssetsNative } from '@/parsers'; import isEmpty from 'lodash/isEmpty'; import isNil from 'lodash/isNil'; @@ -12,13 +12,13 @@ export function selectUserAssetWithUniqueId(uniqueId: string) { }; } -export function selectSortedUserAssets(nativeCurrency: string) { +export function selectSortedUserAssets(nativeCurrency: NativeCurrencyKey) { return (accountAssets: RainbowAddressAssets) => { return sortAssetsByNativeAmount(accountAssets, nativeCurrency); }; } -const sortAssetsByNativeAmount = (accountAssets: RainbowAddressAssets, nativeCurrency: string): ParsedAddressAsset[] => { +const sortAssetsByNativeAmount = (accountAssets: RainbowAddressAssets, nativeCurrency: NativeCurrencyKey): ParsedAddressAsset[] => { let assetsNativePrices = Object.values(accountAssets); if (!isEmpty(assetsNativePrices)) { diff --git a/src/resources/assets/assets.ts b/src/resources/assets/assets.ts index 48cf161d614..37cee5c850b 100644 --- a/src/resources/assets/assets.ts +++ b/src/resources/assets/assets.ts @@ -3,7 +3,6 @@ import isEmpty from 'lodash/isEmpty'; import { MMKV } from 'react-native-mmkv'; import { NativeCurrencyKey, ParsedAddressAsset } from '@/entities'; import { isNativeAsset } from '@/handlers/assets'; -import { Network } from '@/helpers/networkTypes'; import { convertRawAmountToBalance } from '@/helpers/utilities'; import { BooleanMap } from '@/hooks/useCoinListEditOptions'; import { queryClient } from '@/react-query'; @@ -14,11 +13,10 @@ import { RainbowPositions } from '@/resources/defi/types'; import { ethereumUtils } from '@/utils'; import { AddysAddressAsset, AddysAsset, ParsedAsset, RainbowAddressAssets } from './types'; import { getUniqueId } from '@/utils/ethereumUtils'; +import { ChainId } from '@/networks/types'; const storage = new MMKV(); -const MAINNET_CHAIN_ID = ethereumUtils.getChainIdFromNetwork(Network.mainnet); - export const filterPositionsData = ( address: string, currency: NativeCurrencyKey, @@ -43,7 +41,7 @@ export const filterPositionsData = ( export function parseAsset({ address, asset }: { address: string; asset: AddysAsset }): ParsedAsset { const network = asset?.network; const chainId = ethereumUtils.getChainIdFromNetwork(network); - const mainnetAddress = asset?.networks?.[MAINNET_CHAIN_ID]?.address; + const mainnetAddress = asset?.networks?.[ChainId.mainnet]?.address; const uniqueId = getUniqueId(address, chainId); const parsedAsset = { @@ -74,6 +72,7 @@ export function parseAddressAsset({ assetData }: { assetData: AddysAddressAsset const asset = assetData?.asset; const quantity = assetData?.quantity; const address = assetData?.asset?.asset_code; + const parsedAsset = parseAsset({ address, asset, diff --git a/src/resources/assets/externalAssetsQuery.ts b/src/resources/assets/externalAssetsQuery.ts index 0092abdeebc..de29cf9d322 100644 --- a/src/resources/assets/externalAssetsQuery.ts +++ b/src/resources/assets/externalAssetsQuery.ts @@ -4,8 +4,9 @@ import { createQueryKey, queryClient, QueryConfig, QueryFunctionArgs, QueryFunct import { convertAmountAndPriceToNativeDisplay, convertAmountToPercentageDisplay } from '@/helpers/utilities'; import { NativeCurrencyKey } from '@/entities'; import { Token } from '@/graphql/__generated__/metadata'; -import { ethereumUtils } from '@/utils'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; +import { isNativeAsset } from '@/__swaps__/utils/chains'; +import { AddressOrEth } from '@/__swaps__/types/assets'; export const EXTERNAL_TOKEN_CACHE_TIME = 1000 * 60 * 60 * 24; // 24 hours export const EXTERNAL_TOKEN_STALE_TIME = 1000 * 60; // 1 minute @@ -19,7 +20,9 @@ export const EXTERNAL_TOKEN_STALE_TIME = 1000 * 60; // 1 minute // Types type ExternalToken = Pick; export type FormattedExternalAsset = ExternalToken & { + address: string; icon_url?: string; + isNativeAsset: boolean; native: { change: string; price: { @@ -43,9 +46,16 @@ export const externalTokenQueryKey = ({ address, chainId, currency }: ExternalTo type externalTokenQueryKey = ReturnType; // Helpers -const formatExternalAsset = (asset: ExternalToken, nativeCurrency: NativeCurrencyKey): FormattedExternalAsset => { +const formatExternalAsset = ( + address: string, + chainId: ChainId, + asset: ExternalToken, + nativeCurrency: NativeCurrencyKey +): FormattedExternalAsset => { return { ...asset, + address, + isNativeAsset: isNativeAsset(address as AddressOrEth, chainId), native: { change: asset?.price?.relativeChange24h ? convertAmountToPercentageDisplay(`${asset?.price?.relativeChange24h}`) : '', price: convertAmountAndPriceToNativeDisplay(1, asset?.price?.value || 0, nativeCurrency), @@ -62,7 +72,7 @@ export async function fetchExternalToken({ address, chainId, currency }: Externa currency, }); if (response.token) { - return formatExternalAsset(response.token, currency); + return formatExternalAsset(address, chainId, response.token, currency); } else { return null; } diff --git a/src/resources/assets/hardhatAssets.ts b/src/resources/assets/hardhatAssets.ts index 63ba4a2b21f..606dad3c8b3 100644 --- a/src/resources/assets/hardhatAssets.ts +++ b/src/resources/assets/hardhatAssets.ts @@ -1,23 +1,24 @@ import { Contract } from '@ethersproject/contracts'; import { keyBy, mapValues } from 'lodash'; -import { Network } from '@/helpers/networkTypes'; -import { web3Provider } from '@/handlers/web3'; // TODO JIN -import { getNetworkObj } from '@/networks'; +import { getProvider } from '@/handlers/web3'; import { balanceCheckerContractAbi, chainAssets, ETH_ADDRESS, SUPPORTED_CHAIN_IDS } from '@/references'; import { parseAddressAsset } from './assets'; import { RainbowAddressAssets } from './types'; import { logger, RainbowError } from '@/logger'; import { AddressOrEth, UniqueId, ZerionAsset } from '@/__swaps__/types/assets'; -import { ChainId, ChainName } from '@/__swaps__/types/chains'; import { AddressZero } from '@ethersproject/constants'; import chainAssetsByChainId from '@/references/testnet-assets-by-chain'; +import { getNetworkObject } from '@/networks'; +import { ChainId, ChainName, Network } from '@/networks/types'; const fetchHardhatBalancesWithBalanceChecker = async ( tokens: string[], address: string, - network: Network = Network.mainnet + chainId: ChainId = ChainId.mainnet ): Promise<{ [tokenAddress: string]: string } | null> => { - const balanceCheckerContract = new Contract(getNetworkObj(network).balanceCheckerAddress, balanceCheckerContractAbi, web3Provider); + const networkObject = getNetworkObject({ chainId }); + const provider = getProvider({ chainId }); + const balanceCheckerContract = new Contract(networkObject.balanceCheckerAddress, balanceCheckerContractAbi, provider); try { const values = await balanceCheckerContract.balances([address], tokens); const balances: { @@ -42,13 +43,16 @@ const fetchHardhatBalancesWithBalanceChecker = async ( * @param network - The network to fetch the balances for. * @returns The balances of the hardhat assets for the given account address and network. */ -export const fetchHardhatBalances = async (accountAddress: string, network: Network = Network.mainnet): Promise => { - const chainAssetsMap = keyBy(chainAssets[network as keyof typeof chainAssets], ({ asset }) => `${asset.asset_code}_${asset.chainId}`); +export const fetchHardhatBalances = async (accountAddress: string, chainId: ChainId = ChainId.mainnet): Promise => { + const chainAssetsMap = keyBy( + chainAssets[`${chainId}` as keyof typeof chainAssets], + ({ asset }) => `${asset.asset_code}_${asset.chainId}` + ); const tokenAddresses = Object.values(chainAssetsMap).map(({ asset: { asset_code } }) => asset_code === ETH_ADDRESS ? AddressZero : asset_code.toLowerCase() ); - const balances = await fetchHardhatBalancesWithBalanceChecker(tokenAddresses, accountAddress, network); + const balances = await fetchHardhatBalancesWithBalanceChecker(tokenAddresses, accountAddress, chainId); if (!balances) return {}; const updatedAssets = mapValues(chainAssetsMap, chainAsset => { @@ -67,7 +71,7 @@ export const fetchHardhatBalances = async (accountAddress: string, network: Netw export const fetchHardhatBalancesByChainId = async ( accountAddress: string, - network: Network = Network.mainnet + chainId: ChainId = ChainId.mainnet ): Promise<{ assets: { [uniqueId: UniqueId]: { @@ -77,12 +81,13 @@ export const fetchHardhatBalancesByChainId = async ( }; chainIdsInResponse: ChainId[]; }> => { - const chainAssetsMap = chainAssetsByChainId[network as keyof typeof chainAssets] || {}; + const chainAssetsMap = chainAssetsByChainId[`${chainId}` as keyof typeof chainAssets] || {}; + const tokenAddresses = Object.values(chainAssetsMap).map(({ asset }) => asset.asset_code === ETH_ADDRESS ? AddressZero : asset.asset_code.toLowerCase() ); - const balances = await fetchHardhatBalancesWithBalanceChecker(tokenAddresses, accountAddress, network); + const balances = await fetchHardhatBalancesWithBalanceChecker(tokenAddresses, accountAddress, chainId); if (!balances) return { assets: {}, diff --git a/src/resources/assets/types.ts b/src/resources/assets/types.ts index 39a5f41ea6d..a056e695b91 100644 --- a/src/resources/assets/types.ts +++ b/src/resources/assets/types.ts @@ -1,6 +1,6 @@ -import { Network } from '@/helpers/networkTypes'; import { NativeCurrencyKey, ParsedAddressAsset } from '@/entities'; import { TokenColors } from '@/graphql/__generated__/metadata'; +import { Network } from '@/networks/types'; export type AddysAccountAssetsResponse = { meta: AddysAccountAssetsMeta; @@ -54,7 +54,7 @@ export interface ParsedAsset { address: string; color?: string; colors?: TokenColors; - chainId?: number; + chainId: number; chainName?: string; decimals: number; icon_url?: string; diff --git a/src/resources/assets/useSortedUserAssets.ts b/src/resources/assets/useSortedUserAssets.ts index 96cd2e91490..b7e80d92c71 100644 --- a/src/resources/assets/useSortedUserAssets.ts +++ b/src/resources/assets/useSortedUserAssets.ts @@ -1,11 +1,11 @@ -import { getIsHardhatConnected } from '@/handlers/web3'; import { useAccountSettings } from '@/hooks'; import { selectSortedUserAssets } from '@/resources/assets/assetSelectors'; import { useUserAssets } from '@/resources/assets/UserAssetsQuery'; +import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; export function useSortedUserAssets() { const { accountAddress, nativeCurrency } = useAccountSettings(); - const connectedToHardhat = getIsHardhatConnected(); + const { connectedToHardhat } = useConnectedToHardhatStore(); return useUserAssets( { diff --git a/src/resources/assets/useUserAsset.ts b/src/resources/assets/useUserAsset.ts index b94d22c15ec..bad1a672d05 100644 --- a/src/resources/assets/useUserAsset.ts +++ b/src/resources/assets/useUserAsset.ts @@ -1,14 +1,14 @@ -import { ChainId } from '@/__swaps__/types/chains'; -import { getIsHardhatConnected } from '@/handlers/web3'; +import { ChainId } from '@/networks/types'; import { useAccountSettings } from '@/hooks'; import { getNetworkObject } from '@/networks'; import { useUserAssets } from '@/resources/assets/UserAssetsQuery'; import { selectUserAssetWithUniqueId } from '@/resources/assets/assetSelectors'; import { getUniqueId } from '@/utils/ethereumUtils'; +import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; export function useUserAsset(uniqueId: string) { const { accountAddress, nativeCurrency } = useAccountSettings(); - const connectedToHardhat = getIsHardhatConnected(); + const { connectedToHardhat } = useConnectedToHardhatStore(); return useUserAssets( { diff --git a/src/resources/assets/useUserAssetCount.ts b/src/resources/assets/useUserAssetCount.ts index bd414a8c651..7cf00ced409 100644 --- a/src/resources/assets/useUserAssetCount.ts +++ b/src/resources/assets/useUserAssetCount.ts @@ -1,13 +1,13 @@ -import { getIsHardhatConnected } from '@/handlers/web3'; import { useAccountSettings } from '@/hooks'; import { useUserAssets } from '@/resources/assets/UserAssetsQuery'; import { RainbowAddressAssets } from './types'; +import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; const countSelector = (accountAssets: RainbowAddressAssets) => accountAssets?.length; export function useUserAssetCount() { const { accountAddress, nativeCurrency } = useAccountSettings(); - const connectedToHardhat = getIsHardhatConnected(); + const { connectedToHardhat } = useConnectedToHardhatStore(); return useUserAssets( { diff --git a/src/resources/defi/PositionsQuery.ts b/src/resources/defi/PositionsQuery.ts index 8579523d939..124948a1c93 100644 --- a/src/resources/defi/PositionsQuery.ts +++ b/src/resources/defi/PositionsQuery.ts @@ -3,14 +3,14 @@ import { useQuery } from '@tanstack/react-query'; import { createQueryKey, queryClient, QueryConfig, QueryFunctionArgs, QueryFunctionResult } from '@/react-query'; import { NativeCurrencyKey } from '@/entities'; -import { RainbowNetworks } from '@/networks'; +import { RainbowNetworkObjects } from '@/networks'; import { rainbowFetch } from '@/rainbow-fetch'; import { ADDYS_API_KEY } from 'react-native-dotenv'; import { AddysPositionsResponse, PositionsArgs } from './types'; import { parsePositions } from './utils'; export const buildPositionsUrl = (address: string) => { - const networkString = RainbowNetworks.filter(network => network.enabled) + const networkString = RainbowNetworkObjects.filter(network => network.enabled) .map(network => network.id) .join(','); return `https://addys.p.rainbow.me/v3/${networkString}/${address}/positions`; diff --git a/src/resources/ens/ensAddressQuery.ts b/src/resources/ens/ensAddressQuery.ts index f84bb000bb8..a4110e5c5d1 100644 --- a/src/resources/ens/ensAddressQuery.ts +++ b/src/resources/ens/ensAddressQuery.ts @@ -1,7 +1,8 @@ import { useQuery } from '@tanstack/react-query'; import { createQueryKey, queryClient, QueryFunctionArgs } from '@/react-query'; -import { getProviderForNetwork } from '@/handlers/web3'; +import { getProvider } from '@/handlers/web3'; +import { ChainId } from '@/networks/types'; // Set a default stale time of 10 seconds so we don't over-fetch // (query will serve cached data & invalidate after 10s). @@ -23,7 +24,7 @@ const ensAddressQueryKey = ({ name }: ENSAddressArgs) => createQueryKey('ensAddr // Query Function async function ensAddressQueryFunction({ queryKey: [{ name }] }: QueryFunctionArgs) { - const provider = getProviderForNetwork(); + const provider = getProvider({ chainId: ChainId.mainnet }); const address = await provider.resolveName(name); return address; } diff --git a/src/resources/favorites.ts b/src/resources/favorites.ts index 3bb6096f8aa..6a3b918f42a 100644 --- a/src/resources/favorites.ts +++ b/src/resources/favorites.ts @@ -1,8 +1,7 @@ import { AddressOrEth, UniqueId } from '@/__swaps__/types/assets'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId, Network } from '@/networks/types'; import { getStandardizedUniqueIdWorklet } from '@/__swaps__/utils/swaps'; import { NativeCurrencyKeys, RainbowToken } from '@/entities'; -import { Network } from '@/networks/types'; import { createQueryKey, queryClient } from '@/react-query'; import { DAI_ADDRESS, ETH_ADDRESS, SOCKS_ADDRESS, WBTC_ADDRESS, WETH_ADDRESS } from '@/references'; import { promiseUtils } from '@/utils'; diff --git a/src/resources/nfts/index.ts b/src/resources/nfts/index.ts index 5e57fda8bf8..1730cb71592 100644 --- a/src/resources/nfts/index.ts +++ b/src/resources/nfts/index.ts @@ -1,15 +1,14 @@ import { QueryFunction, useQuery } from '@tanstack/react-query'; import { QueryConfigWithSelect, createQueryKey } from '@/react-query'; -import { NFT } from '@/resources/nfts/types'; import { fetchSimpleHashNFTListing } from '@/resources/nfts/simplehash'; import { simpleHashNFTToUniqueAsset } from '@/resources/nfts/simplehash/utils'; import { useSelector } from 'react-redux'; import { AppState } from '@/redux/store'; -import { Network } from '@/helpers'; import { UniqueAsset } from '@/entities'; import { arcClient } from '@/graphql'; import { createSelector } from 'reselect'; import { NftCollectionSortCriterion } from '@/graphql/__generated__/arc'; +import { ChainId } from '@/networks/types'; const NFTS_STALE_TIME = 600000; // 10 minutes const NFTS_CACHE_TIME_EXTERNAL = 3600000; // 1 hour @@ -21,12 +20,12 @@ export const nftsQueryKey = ({ address, sortBy }: { address: string; sortBy: Nft export const nftListingQueryKey = ({ contractAddress, tokenId, - network, + chainId, }: { contractAddress: string; tokenId: string; - network: Omit; -}) => createQueryKey('nftListing', { contractAddress, tokenId, network }); + chainId: Omit; +}) => createQueryKey('nftListing', { contractAddress, tokenId, chainId }); const walletsSelector = (state: AppState) => state.wallets?.wallets; @@ -105,17 +104,17 @@ export function useLegacyNFTs({ export function useNFTListing({ contractAddress, tokenId, - network, + chainId, }: { contractAddress: string; tokenId: string; - network: Omit; + chainId: Omit; }) { return useQuery( - nftListingQueryKey({ contractAddress, tokenId, network }), - async () => (await fetchSimpleHashNFTListing(contractAddress, tokenId, network)) ?? null, + nftListingQueryKey({ contractAddress, tokenId, chainId }), + async () => (await fetchSimpleHashNFTListing(contractAddress, tokenId, chainId)) ?? null, { - enabled: !!network && !!contractAddress && !!tokenId, + enabled: !!chainId && !!contractAddress && !!tokenId, staleTime: 0, cacheTime: 0, } diff --git a/src/resources/nfts/simplehash/index.ts b/src/resources/nfts/simplehash/index.ts index 8e6e9f24271..5d716846c47 100644 --- a/src/resources/nfts/simplehash/index.ts +++ b/src/resources/nfts/simplehash/index.ts @@ -1,11 +1,11 @@ import { NFT_API_KEY, NFT_API_URL } from 'react-native-dotenv'; import { RainbowFetchClient } from '@/rainbow-fetch'; -import { Network } from '@/helpers'; import { SimpleHashListing, SimpleHashNFT, SimpleHashMarketplaceId } from '@/resources/nfts/simplehash/types'; -import { getNetworkObj } from '@/networks'; +import { getNetworkObject } from '@/networks'; import { UniqueAsset } from '@/entities'; import { RainbowError, logger } from '@/logger'; import { getGnosisNetworkObject } from '@/networks/gnosis'; +import { ChainId } from '@/networks/types'; export const START_CURSOR = 'start'; @@ -18,16 +18,16 @@ const createCursorSuffix = (cursor: string) => (cursor === START_CURSOR ? '' : ` export async function fetchSimpleHashNFT( contractAddress: string, tokenId: string, - network: Omit = Network.mainnet + chainId: Omit = ChainId.mainnet ): Promise { - const chain = getNetworkObj(network as Network).nfts.simplehashNetwork; + const simplehashNetwork = getNetworkObject({ chainId: chainId as ChainId })?.nfts?.simplehashNetwork; - if (!chain) { - logger.warn(`[simplehash]: no SimpleHash chain for network: ${network}`); + if (!simplehashNetwork) { + logger.warn(`[simplehash]: no SimpleHash for chainId: ${chainId}`); return; } - const response = await nftApi.get(`/nfts/${chain}/${contractAddress}/${tokenId}`, { + const response = await nftApi.get(`/nfts/${simplehashNetwork}/${contractAddress}/${tokenId}`, { headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', @@ -40,15 +40,15 @@ export async function fetchSimpleHashNFT( export async function fetchSimpleHashNFTListing( contractAddress: string, tokenId: string, - network: Omit = Network.mainnet + chainId: Omit = ChainId.mainnet ): Promise { // array of all eth listings on OpenSea for this token let listings: SimpleHashListing[] = []; let cursor = START_CURSOR; - const chain = getNetworkObj(network as Network).nfts.simplehashNetwork; + const simplehashNetwork = getNetworkObject({ chainId: chainId as ChainId })?.nfts?.simplehashNetwork; - if (!chain) { - logger.warn(`[simplehash]: no SimpleHash chain for network: ${network}`); + if (!simplehashNetwork) { + logger.warn(`[simplehash]: no SimpleHash for chainId: ${chainId}`); return; } @@ -57,7 +57,7 @@ export async function fetchSimpleHashNFTListing( // eslint-disable-next-line no-await-in-loop const response = await nftApi.get( // OpenSea ETH offers only for now - `/nfts/listings/${chain}/${contractAddress}/${tokenId}?marketplaces=${SimpleHashMarketplaceId.OpenSea}${cursorSuffix}`, + `/nfts/listings/${simplehashNetwork}/${contractAddress}/${tokenId}?marketplaces=${SimpleHashMarketplaceId.OpenSea}${cursorSuffix}`, { headers: { 'Accept': 'application/json', @@ -84,16 +84,16 @@ export async function fetchSimpleHashNFTListing( * @param nft */ export async function refreshNFTContractMetadata(nft: UniqueAsset) { - const chain = (nft.isPoap ? getGnosisNetworkObject() : getNetworkObj(nft.network)).nfts.simplehashNetwork; + const simplehashNetwork = (nft.isPoap ? getGnosisNetworkObject() : getNetworkObject({ chainId: nft.chainId }))?.nfts?.simplehashNetwork; - if (!chain) { - logger.warn(`[simplehash]: no SimpleHash chain for network: ${nft.network}`); + if (!simplehashNetwork) { + logger.warn(`[simplehash]: no SimpleHash for chainId: ${nft.chainId}`); return; } try { await nftApi.post( - `/nfts/refresh/${chain}/${nft.asset_contract.address}`, + `/nfts/refresh/${simplehashNetwork}/${nft.asset_contract.address}`, {}, { headers: { @@ -111,7 +111,7 @@ export async function refreshNFTContractMetadata(nft: UniqueAsset) { // If the collection has > 20k NFTs, the above request will fail. // In that case, we need to refresh the given NFT individually. await nftApi.post( - `/nfts/refresh/${chain}/${nft.asset_contract.address}/${nft.id}`, + `/nfts/refresh/${simplehashNetwork}/${nft.asset_contract.address}/${nft.id}`, {}, { headers: { @@ -136,10 +136,10 @@ export async function refreshNFTContractMetadata(nft: UniqueAsset) { * @param nft */ export async function reportNFT(nft: UniqueAsset) { - const chain = (nft.isPoap ? getGnosisNetworkObject() : getNetworkObj(nft.network)).nfts.simplehashNetwork; + const simplehashNetwork = (nft.isPoap ? getGnosisNetworkObject() : getNetworkObject({ chainId: nft.chainId }))?.nfts?.simplehashNetwork; - if (!chain) { - logger.warn(`[simplehash]: no SimpleHash chain for network: ${nft.network}`); + if (!simplehashNetwork) { + logger.warn(`[simplehash]: no SimpleHash for chainId: ${nft.chainId}`); return; } @@ -148,7 +148,7 @@ export async function reportNFT(nft: UniqueAsset) { '/nfts/report/spam', { contract_address: nft.asset_contract.address, - chain_id: chain, + chain_id: simplehashNetwork, token_id: nft.id, event_type: 'mark_as_spam', }, diff --git a/src/resources/nfts/simplehash/types.ts b/src/resources/nfts/simplehash/types.ts index a26cddcf86b..7cc2c802cbe 100644 --- a/src/resources/nfts/simplehash/types.ts +++ b/src/resources/nfts/simplehash/types.ts @@ -1,4 +1,4 @@ -import { Network } from '@/helpers'; +import { Network } from '@/networks/types'; /** * @see https://docs.simplehash.com/reference/sale-model diff --git a/src/resources/nfts/simplehash/utils.ts b/src/resources/nfts/simplehash/utils.ts index 68929492ca2..8cf2c3e8ac6 100644 --- a/src/resources/nfts/simplehash/utils.ts +++ b/src/resources/nfts/simplehash/utils.ts @@ -20,7 +20,7 @@ import { deviceUtils } from '@/utils'; import { TokenStandard } from '@/handlers/web3'; import { handleNFTImages } from '@/utils/handleNFTImages'; import { SimpleHashNft } from '@/graphql/__generated__/arc'; -import { Network } from '@/helpers'; +import { Network, chainNameToIdMapping } from '@/networks/types'; const ENS_COLLECTION_NAME = 'ENS'; const SVG_MIME_TYPE = 'image/svg+xml'; @@ -78,6 +78,7 @@ export function simpleHashNFTToUniqueAsset(nft: SimpleHashNft, address: string): slug: marketplace?.marketplace_collection_id ?? '', twitter_username: collection.twitter_username, }, + chainId: chainNameToIdMapping[nft.chain as keyof typeof chainNameToIdMapping], description: nft.description, external_link: nft.external_url, familyImage: collection.image_url, diff --git a/src/resources/nfts/types.ts b/src/resources/nfts/types.ts index aafbe3f300a..5f92ca09560 100644 --- a/src/resources/nfts/types.ts +++ b/src/resources/nfts/types.ts @@ -1,5 +1,5 @@ -import { Network } from '@/helpers/networkTypes'; import { Asset, AssetContract, AssetType } from '@/entities'; +import { Network } from '@/networks/types'; import { UniqueTokenType } from '@/utils/uniqueTokens'; export enum NFTMarketplaceId { diff --git a/src/resources/nfts/utils.ts b/src/resources/nfts/utils.ts index 23b0d94af41..587bd19d742 100644 --- a/src/resources/nfts/utils.ts +++ b/src/resources/nfts/utils.ts @@ -3,9 +3,9 @@ import { gretch } from 'gretchen'; import { paths } from '@reservoir0x/reservoir-sdk'; import { RainbowError, logger } from '@/logger'; import { handleSignificantDecimals } from '@/helpers/utilities'; -import { Network } from '@/helpers'; import { IS_PROD } from '@/env'; import { RESERVOIR_API_KEY_DEV, RESERVOIR_API_KEY_PROD } from 'react-native-dotenv'; +import { Network } from '@/networks/types'; const SUPPORTED_NETWORKS = [Network.mainnet, Network.polygon, Network.bsc, Network.arbitrum, Network.optimism, Network.base, Network.zora]; diff --git a/src/resources/reservoir/mints.ts b/src/resources/reservoir/mints.ts index 0c667846440..1da3370c832 100644 --- a/src/resources/reservoir/mints.ts +++ b/src/resources/reservoir/mints.ts @@ -1,13 +1,12 @@ import { EthereumAddress } from '@/entities'; import { arcClient } from '@/graphql'; -import { getNetworkObj } from '@/networks'; import { Navigation } from '@/navigation'; -import { Network } from '@/networks/types'; import Routes from '@/navigation/routesNames'; -import { logger, RainbowError } from '@/logger'; +import { logger } from '@/logger'; import { WrappedAlert as Alert } from '@/helpers/alert'; import * as lang from '@/languages'; import { BigNumberish } from '@ethersproject/bignumber'; +import { ChainId } from '@/networks/types'; const showAlert = () => { Alert.alert( @@ -17,14 +16,14 @@ const showAlert = () => { { cancelable: false } ); }; -export const navigateToMintCollection = async (contractAddress: EthereumAddress, pricePerMint: BigNumberish, network: Network) => { + +export const navigateToMintCollection = async (contractAddress: EthereumAddress, pricePerMint: BigNumberish, chainId: ChainId) => { logger.debug('[mints]: Navigating to Mint Collection', { contractAddress, - network, + chainId, }); try { - const chainId = getNetworkObj(network).id; const res = await arcClient.getReservoirCollection({ contractAddress, chainId, @@ -35,13 +34,13 @@ export const navigateToMintCollection = async (contractAddress: EthereumAddress, pricePerMint, }); } else { - logger.warn('[mints]: No collection found', { contractAddress, network }); + logger.warn('[mints]: No collection found', { contractAddress, chainId }); showAlert(); } } catch (e) { logger.warn(`[mints]: navigateToMintCollection error`, { contractAddress, - network, + chainId, error: e, }); showAlert(); diff --git a/src/resources/reservoir/utils.ts b/src/resources/reservoir/utils.ts index ec4dcf2140c..4e027b18c67 100644 --- a/src/resources/reservoir/utils.ts +++ b/src/resources/reservoir/utils.ts @@ -1,4 +1,4 @@ -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; const RAINBOW_FEE_ADDRESS_MAINNET = '0x69d6d375de8c7ade7e44446df97f49e661fdad7d'; const RAINBOW_FEE_ADDRESS_POLYGON = '0xfb9af3db5e19c4165f413f53fe3bbe6226834548'; diff --git a/src/resources/transactions/consolidatedTransactions.ts b/src/resources/transactions/consolidatedTransactions.ts index 34208c52bc0..2d01ef35f24 100644 --- a/src/resources/transactions/consolidatedTransactions.ts +++ b/src/resources/transactions/consolidatedTransactions.ts @@ -5,7 +5,7 @@ import { TransactionApiResponse, TransactionsReceivedMessage } from './types'; import { RainbowError, logger } from '@/logger'; import { rainbowFetch } from '@/rainbow-fetch'; import { ADDYS_API_KEY } from 'react-native-dotenv'; -import { RainbowNetworks } from '@/networks'; +import { RainbowNetworkObjects } from '@/networks'; import { parseTransaction } from '@/parsers/transactions'; import { ethereumUtils } from '@/utils'; @@ -125,7 +125,7 @@ export function useConsolidatedTransactions( { address, currency }: Pick, config: InfiniteQueryConfig = {} ) { - const chainIds = RainbowNetworks.filter(network => network.enabled && network.networkType !== 'testnet').map(network => network.id); + const chainIds = RainbowNetworkObjects.filter(network => network.enabled && network.networkType !== 'testnet').map(network => network.id); return useInfiniteQuery( consolidatedTransactionsQueryKey({ diff --git a/src/resources/transactions/firstTransactionTimestampQuery.ts b/src/resources/transactions/firstTransactionTimestampQuery.ts index 033c295dad8..c1bbbc42888 100644 --- a/src/resources/transactions/firstTransactionTimestampQuery.ts +++ b/src/resources/transactions/firstTransactionTimestampQuery.ts @@ -1,5 +1,4 @@ import { useQuery } from '@tanstack/react-query'; -import PQueue from 'p-queue/dist'; import { createQueryKey, queryClient, QueryConfig, QueryFunctionArgs, QueryFunctionResult } from '@/react-query'; import { getFirstTransactionTimestamp } from '@/utils/ethereumUtils'; @@ -23,8 +22,6 @@ export type FirstTransactionTimestampQueryKey = ReturnType) { @@ -35,7 +32,10 @@ export async function firstTransactionTimestampQueryFunction({ address = (await fetchENSAddress({ name: addressOrName })) ?? ''; } - const timestamp = address ? await queue.add(async () => getFirstTransactionTimestamp(address)) : null; + let timestamp; + if (address) { + timestamp = await getFirstTransactionTimestamp(address); + } return timestamp ?? null; } diff --git a/src/resources/transactions/transaction.ts b/src/resources/transactions/transaction.ts index dd10966baea..ded3b12f7a9 100644 --- a/src/resources/transactions/transaction.ts +++ b/src/resources/transactions/transaction.ts @@ -3,13 +3,13 @@ import { createQueryKey, queryClient, QueryFunctionArgs, QueryFunctionResult } f import { useQuery } from '@tanstack/react-query'; import { consolidatedTransactionsQueryFunction, consolidatedTransactionsQueryKey } from './consolidatedTransactions'; import { useAccountSettings } from '@/hooks'; -import { RainbowNetworks, getNetworkObj } from '@/networks'; +import { RainbowNetworkObjects } from '@/networks'; import { rainbowFetch } from '@/rainbow-fetch'; import { ADDYS_API_KEY } from 'react-native-dotenv'; import { parseTransaction } from '@/parsers/transactions'; -import { Network } from '@/networks/types'; import { RainbowError, logger } from '@/logger'; import { TransactionApiResponse } from './types'; +import { ChainId } from '@/networks/types'; export type ConsolidatedTransactionsResult = QueryFunctionResult; export type PaginatedTransactions = { pages: ConsolidatedTransactionsResult[] }; @@ -18,25 +18,24 @@ export type TransactionArgs = { hash: string; address: string; currency: NativeCurrencyKey; - network: Network; + chainId: ChainId; }; type TransactionQueryKey = ReturnType; export type BackendTransactionArgs = { hash: string; - network: Network; + chainId: ChainId; enabled: boolean; }; -export const transactionQueryKey = ({ hash, address, currency, network }: TransactionArgs) => - createQueryKey('transactions', { address, currency, network, hash }, { persisterVersion: 1 }); +export const transactionQueryKey = ({ hash, address, currency, chainId }: TransactionArgs) => + createQueryKey('transactions', { address, currency, chainId, hash }, { persisterVersion: 1 }); export const fetchTransaction = async ({ - queryKey: [{ address, currency, network, hash }], + queryKey: [{ address, currency, chainId, hash }], }: QueryFunctionArgs): Promise => { try { - const chainId = getNetworkObj(network).id; const url = `https://addys.p.rainbow.me/v3/${chainId}/${address}/transactions/${hash}`; const response = await rainbowFetch<{ payload: { transaction: TransactionApiResponse } }>(url, { method: 'get', @@ -70,19 +69,19 @@ export const fetchTransaction = async ({ export const transactionFetchQuery = async ({ address, currency, - network, + chainId, hash, }: { address: string; currency: NativeCurrencyKey; - network: Network; + chainId: ChainId; hash: string; -}) => queryClient.fetchQuery(transactionQueryKey({ address, currency, network, hash }), fetchTransaction); +}) => queryClient.fetchQuery(transactionQueryKey({ address, currency, chainId, hash }), fetchTransaction); -export function useBackendTransaction({ hash, network }: BackendTransactionArgs) { +export function useBackendTransaction({ hash, chainId }: BackendTransactionArgs) { const { accountAddress, nativeCurrency } = useAccountSettings(); - const chainIds = RainbowNetworks.filter(network => network.enabled && network.networkType !== 'testnet').map(network => network.id); + const chainIds = RainbowNetworkObjects.filter(network => network.enabled && network.networkType !== 'testnet').map(network => network.id); const paginatedTransactionsKey = consolidatedTransactionsQueryKey({ address: accountAddress, @@ -94,11 +93,11 @@ export function useBackendTransaction({ hash, network }: BackendTransactionArgs) hash: hash, address: accountAddress, currency: nativeCurrency, - network: network, + chainId: chainId, }; return useQuery(transactionQueryKey(params), fetchTransaction, { - enabled: !!hash && !!accountAddress && !!network, + enabled: !!hash && !!accountAddress && !!chainId, initialData: () => { const queryData = queryClient.getQueryData(paginatedTransactionsKey); const pages = queryData?.pages || []; @@ -114,15 +113,15 @@ export function useBackendTransaction({ hash, network }: BackendTransactionArgs) }); } -export const useTransaction = ({ network, hash }: { network: Network; hash: string }) => { +export const useTransaction = ({ chainId, hash }: { chainId: ChainId; hash: string }) => { const { data: backendTransaction, isLoading: backendTransactionIsLoading, isFetched: backendTransactionIsFetched, } = useBackendTransaction({ hash, - network, - enabled: !!hash && !!network, + chainId, + enabled: !!hash && !!chainId, }); return { diff --git a/src/resources/transactions/transactionSimulation.ts b/src/resources/transactions/transactionSimulation.ts new file mode 100644 index 00000000000..c8e0a556ccd --- /dev/null +++ b/src/resources/transactions/transactionSimulation.ts @@ -0,0 +1,146 @@ +import { createQueryKey, QueryConfig, QueryFunctionArgs } from '@/react-query'; +import { useQuery } from '@tanstack/react-query'; +import { RainbowError, logger } from '@/logger'; +import { metadataPOSTClient } from '@/graphql'; +import { TransactionErrorType, TransactionScanResultType, TransactionSimulationResult } from '@/graphql/__generated__/metadataPOST'; +import { isNil } from 'lodash'; +import { RequestData } from '@/redux/requests'; +import { ChainId } from '@/networks/types'; + +type SimulationArgs = { + address: string; + chainId: ChainId; + isMessageRequest: boolean; + nativeCurrency: string; + req: any; // Replace 'any' with the correct type for 'req' + requestMessage: string; + simulationUnavailable: boolean; + transactionDetails: RequestData; +}; + +type SimulationResult = { + simulationData: TransactionSimulationResult | undefined; + simulationError: TransactionErrorType | undefined; + simulationScanResult: TransactionScanResultType | undefined; +}; + +const simulationQueryKey = ({ + address, + chainId, + isMessageRequest, + nativeCurrency, + req, + requestMessage, + simulationUnavailable, + transactionDetails, +}: SimulationArgs) => + createQueryKey( + 'txSimulation', + { + address, + chainId, + isMessageRequest, + nativeCurrency, + req, + requestMessage, + simulationUnavailable, + transactionDetails, + }, + { persisterVersion: 1 } + ); + +const fetchSimulation = async ({ + queryKey: [{ address, chainId, isMessageRequest, nativeCurrency, req, requestMessage, simulationUnavailable, transactionDetails }], +}: QueryFunctionArgs): Promise => { + try { + let simulationData; + + if (isMessageRequest) { + simulationData = await metadataPOSTClient.simulateMessage({ + address, + chainId, + message: { + method: transactionDetails?.payload?.method, + params: [requestMessage], + }, + domain: transactionDetails?.dappUrl, + }); + + if (isNil(simulationData?.simulateMessage?.simulation) && isNil(simulationData?.simulateMessage?.error)) { + return { + simulationData: { in: [], out: [], approvals: [] }, + simulationError: undefined, + simulationScanResult: simulationData?.simulateMessage?.scanning?.result, + }; + } else if (simulationData?.simulateMessage?.error && !simulationUnavailable) { + return { + simulationData: undefined, + simulationError: simulationData?.simulateMessage?.error?.type, + simulationScanResult: simulationData?.simulateMessage?.scanning?.result, + }; + } else if (simulationData.simulateMessage?.simulation && !simulationUnavailable) { + return { + simulationData: simulationData.simulateMessage?.simulation, + simulationError: undefined, + simulationScanResult: simulationData?.simulateMessage?.scanning?.result, + }; + } + } else { + simulationData = await metadataPOSTClient.simulateTransactions({ + chainId, + currency: nativeCurrency?.toLowerCase(), + transactions: [ + { + from: req?.from, + to: req?.to, + data: req?.data || '0x', + value: req?.value || '0x0', + }, + ], + domain: transactionDetails?.dappUrl, + }); + + if (isNil(simulationData?.simulateTransactions?.[0]?.simulation) && isNil(simulationData?.simulateTransactions?.[0]?.error)) { + return { + simulationData: { in: [], out: [], approvals: [] }, + simulationError: undefined, + simulationScanResult: simulationData?.simulateTransactions?.[0]?.scanning?.result, + }; + } else if (simulationData?.simulateTransactions?.[0]?.error) { + return { + simulationData: undefined, + simulationError: simulationData?.simulateTransactions?.[0]?.error?.type, + simulationScanResult: simulationData?.simulateTransactions[0]?.scanning?.result, + }; + } else if (simulationData.simulateTransactions?.[0]?.simulation) { + return { + simulationData: simulationData.simulateTransactions[0]?.simulation, + simulationError: undefined, + simulationScanResult: simulationData?.simulateTransactions[0]?.scanning?.result, + }; + } + } + + return { + simulationData: undefined, + simulationError: undefined, + simulationScanResult: undefined, + }; + } catch (error) { + logger.error(new RainbowError('Error while simulating'), { error }); + throw error; + } +}; + +export const useSimulation = ( + args: SimulationArgs, + config: QueryConfig> = {} +) => { + return useQuery(simulationQueryKey(args), fetchSimulation, { + enabled: !!args.address && !!args.chainId, + retry: 3, + refetchOnWindowFocus: false, + staleTime: Infinity, + ...config, + }); +}; diff --git a/src/screens/AddCash/components/ProviderCard.tsx b/src/screens/AddCash/components/ProviderCard.tsx index cbe8ffcb9a1..9dae223ca5f 100644 --- a/src/screens/AddCash/components/ProviderCard.tsx +++ b/src/screens/AddCash/components/ProviderCard.tsx @@ -5,20 +5,18 @@ import chroma from 'chroma-js'; import { IS_IOS } from '@/env'; import { Box, Text, Inline, Bleed, useBackgroundColor } from '@/design-system'; -import { Network } from '@/helpers/networkTypes'; import ChainBadge from '@/components/coin-icon/ChainBadge'; import { Ramp as RampLogo } from '@/components/icons/svg/Ramp'; -import { Ratio as RatioLogo } from '@/components/icons/svg/Ratio'; import { Coinbase as CoinbaseLogo } from '@/components/icons/svg/Coinbase'; import { Moonpay as MoonpayLogo } from '@/components/icons/svg/Moonpay'; import { FiatProviderName } from '@/entities/f2c'; -import { convertAPINetworkToInternalNetwork } from '@/screens/AddCash/utils'; +import { convertAPINetworkToInternalChainIds } from '@/screens/AddCash/utils'; import { ProviderConfig, CalloutType, PaymentMethod } from '@/screens/AddCash/types'; import * as i18n from '@/languages'; import { EthCoinIcon } from '@/components/coin-icon/EthCoinIcon'; -import { ethereumUtils } from '@/utils'; +import { ChainId } from '@/networks/types'; type PaymentMethodConfig = { name: string; @@ -81,26 +79,22 @@ function getPaymentMethodConfigs(paymentMethods: { type: PaymentMethod }[]) { return methods; } -function NetworkIcons({ networks }: { networks: Network[] }) { +function NetworkIcons({ chainIds }: { chainIds?: ChainId[] }) { return ( - {networks.map((network, index) => { + {chainIds?.map((chainId, index) => { return ( 0 ? -6 : 0 }} style={{ position: 'relative', - zIndex: networks.length - index, + zIndex: chainIds.length - index, borderRadius: 30, }} > - {network !== Network.mainnet ? ( - - ) : ( - - )} + {chainId !== ChainId.mainnet ? : } ); })} @@ -233,7 +227,7 @@ export function ProviderCard({ config }: { config: ProviderConfig }) { ); diff --git a/src/screens/AddCash/utils.ts b/src/screens/AddCash/utils.ts index cedf027759e..aa4508030c3 100644 --- a/src/screens/AddCash/utils.ts +++ b/src/screens/AddCash/utils.ts @@ -1,14 +1,14 @@ -import { Network } from '@/helpers/networkTypes'; +import { ChainId } from '@/networks/types'; import { Network as APINetwork } from '@/screens/AddCash/types'; -export function convertAPINetworkToInternalNetwork(network: APINetwork): Network | undefined { +export function convertAPINetworkToInternalChainIds(network: APINetwork): ChainId | undefined { const networkMap = { - [APINetwork.Ethereum]: Network.mainnet, - [APINetwork.Arbitrum]: Network.arbitrum, - [APINetwork.Optimism]: Network.optimism, - [APINetwork.Polygon]: Network.polygon, - [APINetwork.Base]: Network.base, - [APINetwork.BSC]: Network.bsc, + [APINetwork.Ethereum]: ChainId.mainnet, + [APINetwork.Arbitrum]: ChainId.arbitrum, + [APINetwork.Optimism]: ChainId.optimism, + [APINetwork.Polygon]: ChainId.polygon, + [APINetwork.Base]: ChainId.base, + [APINetwork.BSC]: ChainId.bsc, }; // @ts-ignore diff --git a/src/screens/CurrencySelectModal.tsx b/src/screens/CurrencySelectModal.tsx index 25abf58d032..9d141f2b049 100644 --- a/src/screens/CurrencySelectModal.tsx +++ b/src/screens/CurrencySelectModal.tsx @@ -15,7 +15,7 @@ import { Modal } from '../components/modal'; import { STORAGE_IDS } from '../model/mmkv'; import { analytics } from '@/analytics'; import { addHexPrefix, isL2Chain } from '@/handlers/web3'; -import { CurrencySelectionTypes, Network, TokenSectionTypes } from '@/helpers'; +import { CurrencySelectionTypes, TokenSectionTypes } from '@/helpers'; import { useAccountSettings, useInteraction, @@ -40,7 +40,7 @@ import DiscoverSearchInput from '@/components/discover/DiscoverSearchInput'; import { externalTokenQueryKey, fetchExternalToken } from '@/resources/assets/externalAssetsQuery'; import { getNetworkFromChainId } from '@/utils/ethereumUtils'; import { queryClient } from '@/react-query/queryClient'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId, Network } from '@/networks/types'; export interface EnrichedExchangeAsset extends SwappableAsset { ens: boolean; @@ -151,15 +151,11 @@ export default function CurrencySelectModal() { (newAsset: any, selectAsset: any, type: any) => { const otherAsset = type === 'input' ? outputCurrency : inputCurrency; const hasShownWarning = getHasShownWarning(); - if ( - otherAsset && - ethereumUtils.getChainIdFromNetwork(newAsset?.network) !== ethereumUtils.getChainIdFromNetwork(otherAsset?.network) && - !hasShownWarning - ) { + if (otherAsset && newAsset?.chainId !== otherAsset?.chainId && !hasShownWarning) { Keyboard.dismiss(); InteractionManager.runAfterInteractions(() => { navigate(Routes.EXPLAIN_SHEET, { - network: newAsset?.network, + chainId: newAsset?.chainId, onClose: () => { setHasShownWarning(); selectAsset(); @@ -213,6 +209,7 @@ export default function CurrencySelectModal() { name: 'Unswappable', symbol: 'UNSWAP', network: Network.mainnet, + chainId: ChainId.mainnet, id: 'foobar', uniqueId: '0x123', }); @@ -293,14 +290,14 @@ export default function CurrencySelectModal() { screen: Routes.MAIN_EXCHANGE_SCREEN, }); setSearchQuery(''); - setCurrentChainId(ethereumUtils.getChainIdFromNetwork(item.network)); + setCurrentChainId(item.chainId); }, android ? 500 : 0 ); } else { navigate(Routes.MAIN_EXCHANGE_SCREEN); setSearchQuery(''); - setCurrentChainId(ethereumUtils.getChainIdFromNetwork(item.network)); + setCurrentChainId(item.chainId); } if (searchQueryForSearch) { analytics.track('Selected a search result in Swap', { @@ -326,8 +323,7 @@ export default function CurrencySelectModal() { InteractionManager.runAfterInteractions(() => { navigate(Routes.EXPLAIN_SHEET, { assetName: item?.symbol, - network: ethereumUtils.getNetworkFromChainId(currentChainId), - networkName: currentL2Name, + chainId: currentChainId, onClose: linkToHop, type: 'obtainL2Assets', }); @@ -430,11 +426,10 @@ export default function CurrencySelectModal() { const handleBackButton = useCallback(() => { setSearchQuery(''); InteractionManager.runAfterInteractions(() => { - const inputChainId = ethereumUtils.getChainIdFromNetwork(inputCurrency?.network); - setCurrentChainId(inputChainId); + setCurrentChainId(inputCurrency?.chainId); }); setIsTransitioning(true); // continue to display list while transitiong back - }, [inputCurrency?.network]); + }, [inputCurrency?.chainId]); useEffect(() => { // check if list has items before attempting to scroll diff --git a/src/screens/ENSConfirmRegisterSheet.tsx b/src/screens/ENSConfirmRegisterSheet.tsx index beb2f56167c..82daf89ba8e 100644 --- a/src/screens/ENSConfirmRegisterSheet.tsx +++ b/src/screens/ENSConfirmRegisterSheet.tsx @@ -40,7 +40,7 @@ import { usePersistentDominantColorFromImage } from '@/hooks/usePersistentDomina import { handleReviewPromptAction } from '@/utils/reviewAlert'; import { ReviewPromptAction } from '@/storage/schema'; import { ActionTypes } from '@/hooks/useENSRegistrationActionHandler'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; export const ENSConfirmRegisterSheetHeight = 600; export const ENSConfirmRenewSheetHeight = 560; diff --git a/src/screens/ExchangeModal.tsx b/src/screens/ExchangeModal.tsx index 304fcfe7764..86441c24004 100644 --- a/src/screens/ExchangeModal.tsx +++ b/src/screens/ExchangeModal.tsx @@ -27,7 +27,7 @@ import { WrappedAlert as Alert } from '@/helpers/alert'; import { analytics } from '@/analytics'; import { Box, Row, Rows } from '@/design-system'; import { GasFee, LegacyGasFee, LegacyGasFeeParams, SwappableAsset } from '@/entities'; -import { ExchangeModalTypes, isKeyboardOpen, Network } from '@/helpers'; +import { ExchangeModalTypes, isKeyboardOpen } from '@/helpers'; import { KeyboardType } from '@/helpers/keyboardTypes'; import { getFlashbotsProvider, getProvider } from '@/handlers/web3'; import { delay, greaterThan } from '@/helpers/utilities'; @@ -70,7 +70,7 @@ import { ReviewPromptAction } from '@/storage/schema'; import { SwapPriceImpactType } from '@/hooks/usePriceImpactDetails'; import { getNextNonce } from '@/state/nonces'; import { getChainName } from '@/__swaps__/utils/chains'; -import { ChainId, ChainName } from '@/__swaps__/types/chains'; +import { ChainId, ChainName } from '@/networks/types'; import { AddressOrEth, ParsedAsset } from '@/__swaps__/types/assets'; import { TokenColors } from '@/graphql/__generated__/metadata'; import { estimateSwapGasLimit } from '@/raps/actions'; @@ -82,7 +82,7 @@ export const DEFAULT_SLIPPAGE_BIPS = { [ChainId.polygon]: 200, [ChainId.base]: 200, [ChainId.bsc]: 200, - [Network.optimism]: 200, + [ChainId.optimism]: 200, [ChainId.arbitrum]: 200, [ChainId.goerli]: 100, [ChainId.gnosis]: 200, @@ -145,15 +145,14 @@ export function ExchangeModal({ fromDiscover, ignoreInitialTypeCheck, testID, ty updateDefaultGasLimit, updateGasFeeOption, updateTxFee, - txNetwork, - + chainId, isGasReady, } = useGas(); const { accountAddress, flashbotsEnabled, nativeCurrency } = useAccountSettings(); const [isAuthorizing, setIsAuthorizing] = useState(false); const prevGasFeesParamsBySpeed = usePrevious(gasFeeParamsBySpeed); - const prevChainId = usePrevious(ethereumUtils.getChainIdFromNetwork(txNetwork)); + const prevChainId = usePrevious(chainId); const keyboardListenerSubscription = useRef(); @@ -222,7 +221,7 @@ export function ExchangeModal({ fromDiscover, ignoreInitialTypeCheck, testID, ty if (currentChainId !== prevChainId) { speedUrgentSelected.current = false; } - }, [currentChainId, prevChainId, txNetwork]); + }, [currentChainId, prevChainId]); const defaultGasLimit = useMemo(() => { return ethereumUtils.getBasicSwapGasLimit(Number(currentChainId)); @@ -347,14 +346,14 @@ export function ExchangeModal({ fromDiscover, ignoreInitialTypeCheck, testID, ty ) { updateGasLimit(); } - }, [currentChainId, gasFeeParamsBySpeed, isGasReady, prevChainId, prevGasFeesParamsBySpeed, txNetwork, updateGasLimit]); + }, [currentChainId, gasFeeParamsBySpeed, isGasReady, prevChainId, prevGasFeesParamsBySpeed, updateGasLimit]); // Listen to gas prices, Uniswap reserves updates useEffect(() => { updateDefaultGasLimit(defaultGasLimit); InteractionManager.runAfterInteractions(() => { // Start polling in the current network - startPollingGasFees(ethereumUtils.getNetworkFromChainId(currentChainId), flashbots); + startPollingGasFees(currentChainId, flashbots); }); return () => { stopPollingGasFees(); @@ -432,7 +431,7 @@ export function ExchangeModal({ fromDiscover, ignoreInitialTypeCheck, testID, ty } logger.debug(`[ExchangeModal]: getting nonce for account ${accountAddress}`); - const currentNonce = await getNextNonce({ address: accountAddress, network: ethereumUtils.getNetworkFromChainId(currentChainId) }); + const currentNonce = await getNextNonce({ address: accountAddress, chainId: currentChainId }); logger.debug(`[ExchangeModal]: nonce for account ${accountAddress} is ${currentNonce}`); const { independentField, independentValue, slippageInBips, source } = store.getState().swap; @@ -649,7 +648,7 @@ export function ExchangeModal({ fromDiscover, ignoreInitialTypeCheck, testID, ty const confirmButtonProps = useMemoOne( () => ({ - currentNetwork: ethereumUtils.getNetworkFromChainId(currentChainId), + chainId: currentChainId, disabled: !Number(inputAmount) || (!loading && !tradeDetails), inputAmount, isAuthorizing, @@ -691,7 +690,7 @@ export function ExchangeModal({ fromDiscover, ignoreInitialTypeCheck, testID, ty setParams({ focused: false }); navigate(Routes.SWAP_SETTINGS_SHEET, { asset: outputCurrency, - network: ethereumUtils.getNetworkFromChainId(currentChainId), + chainId: currentChainId, restoreFocusOnSwapModal: () => { android && (lastFocusedInputHandle.current = lastFocusedInputHandleTemporary); setParams({ focused: true }); @@ -781,8 +780,8 @@ export function ExchangeModal({ fromDiscover, ignoreInitialTypeCheck, testID, ty lastFocusedInput?.blur(); navigate(Routes.EXPLAIN_SHEET, { inputToken: inputCurrency?.symbol, - fromNetwork: ethereumUtils.getNetworkFromChainId(inputChainId), - toNetwork: ethereumUtils.getNetworkFromChainId(outputChainId), + fromChainId: inputChainId, + toChainId: outputChainId, isCrosschainSwap, isBridgeSwap, onClose: () => { diff --git a/src/screens/ExplainSheet.js b/src/screens/ExplainSheet.js index d5ce85ef357..f92f02ea5a9 100644 --- a/src/screens/ExplainSheet.js +++ b/src/screens/ExplainSheet.js @@ -1,3 +1,4 @@ +/* eslint-disable react/jsx-props-no-spreading */ import { useRoute } from '@react-navigation/native'; import lang from 'i18n-js'; import React, { useCallback, useMemo } from 'react'; @@ -10,21 +11,19 @@ import { Emoji, GradientText, Text } from '../components/text'; import { useNavigation } from '../navigation/Navigation'; import { DoubleChevron } from '@/components/icons'; import { Box } from '@/design-system'; -import networkTypes from '@/helpers/networkTypes'; import { useDimensions } from '@/hooks'; import styled from '@/styled-thing'; import { fonts, fontWithWidth, padding, position } from '@/styles'; -import { ethereumUtils, gasUtils, getTokenMetadata } from '@/utils'; +import { ethereumUtils, gasUtils } from '@/utils'; import { buildRainbowLearnUrl } from '@/utils/buildRainbowUrl'; import { cloudPlatformAccountName } from '@/utils/platform'; import { useTheme } from '@/theme'; import { isL2Chain } from '@/handlers/web3'; import { IS_ANDROID } from '@/env'; import * as i18n from '@/languages'; -import { getNetworkObj } from '@/networks'; import { EthCoinIcon } from '@/components/coin-icon/EthCoinIcon'; import RainbowCoinIcon from '@/components/coin-icon/RainbowCoinIcon'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId, chainIdToNameMapping } from '@/networks/types'; const { GAS_TRENDS } = gasUtils; export const ExplainSheetHeight = android ? 454 : 434; @@ -78,8 +77,8 @@ const FLOOR_PRICE_EXPLAINER = lang.t('explain.floor_price.text'); const gasExplainer = network => lang.t('explain.gas.text', { networkName: network }); -const availableNetworksExplainer = (tokenSymbol, networks) => { - const readableNetworks = networks?.map(network => getNetworkObj(network).name)?.join(', '); +const availableNetworksExplainer = (tokenSymbol, chainIds) => { + const readableNetworks = chainIds?.map(chainId => chainIdToNameMapping[chainId])?.join(', '); return lang.t('explain.available_networks.text', { tokenSymbol: tokenSymbol, @@ -158,8 +157,11 @@ const ENS_CONFIGURATION_EXPLAINER = export const explainers = (params, theme) => { const colors = theme?.colors; - const fromNetworkObject = getNetworkObj(params?.fromNetwork); - const toNetworkObject = getNetworkObj(params?.toNetwork); + const chainId = params?.chainId; + const network = ethereumUtils.getNetworkFromChainId(chainId); + const networkName = chainIdToNameMapping[chainId]; + const fromChainId = params?.fromChainId; + const toChainId = params?.toChainId; return { op_rewards_airdrop_timing: { emoji: '📦', @@ -210,7 +212,7 @@ export const explainers = (params, theme) => { title: params?.inputToken ? lang.t(`explain.output_disabled.${params?.isCrosschainSwap ? 'title_crosschain' : 'title'}`, { inputToken: params?.inputToken, - fromNetwork: fromNetworkObject.name, + fromNetwork: chainIdToNameMapping[fromChainId], }) : lang.t('explain.output_disabled.title_empty'), @@ -218,18 +220,18 @@ export const explainers = (params, theme) => { ? lang.t(`explain.output_disabled.${params?.isBridgeSwap ? 'text_bridge' : 'text_crosschain'}`, { inputToken: params?.inputToken, outputToken: params?.outputToken, - fromNetwork: fromNetworkObject.name, - toNetwork: toNetworkObject.name, + fromNetwork: chainIdToNameMapping[fromChainId], + toNetwork: chainIdToNameMapping[toChainId], }) : lang.t('explain.output_disabled.text', { - fromNetwork: fromNetworkObject?.name, + fromNetwork: chainIdToNameMapping[fromChainId]?.name, inputToken: params?.inputToken, outputToken: params?.outputToken, }), - logo: !isL2Chain({ chainId: fromNetworkObject.id }) ? ( + logo: !isL2Chain({ chainId: fromChainId }) ? ( ) : ( - + ), }, floor_price: { @@ -244,15 +246,15 @@ export const explainers = (params, theme) => { size={40} icon={params?.nativeAsset?.icon_url} symbol={params?.nativeAsset?.symbol} - chainId={ethereumUtils.getChainIdFromNetwork(params?.network)} + chainId={chainId} colors={params?.nativeAsset?.colors} theme={theme} /> ), extraHeight: 2, - text: gasExplainer(getNetworkObj(params?.network).name), + text: gasExplainer(chainIdToNameMapping[chainId]), title: lang.t('explain.gas.title', { - networkName: getNetworkObj(params?.network).name, + networkName: chainIdToNameMapping[chainId], }), }, ens_primary_name: { @@ -533,22 +535,17 @@ export const explainers = (params, theme) => { }, swapResetInputs: { button: { - label: `Continue with ${getNetworkObj(params?.network)?.name}`, - bgColor: colors?.networkColors[params?.network] && colors?.alpha(colors?.networkColors[params?.network], 0.06), - textColor: colors?.networkColors[params?.network] && colors?.networkColors?.[params?.network], + label: `Continue with ${networkName}`, + bgColor: colors?.networkColors[chainId] && colors?.alpha(colors?.networkColors[chainId], 0.06), + textColor: colors?.networkColors[chainId] && colors?.networkColors?.[network], }, emoji: '🔐', extraHeight: -90, text: SWAP_RESET_EXPLAINER, - title: `Switching to ${getNetworkObj(params?.network)?.name}`, + title: `Switching to ${networkName}`, logo: - params?.network !== 'mainnet' ? ( - + chainId !== ChainId.mainnet ? ( + ) : ( ), @@ -596,7 +593,7 @@ export const explainers = (params, theme) => { size={40} icon={params?.inputCurrency?.icon_url} symbol={params?.inputCurrency?.symbol} - chainId={ethereumUtils.getChainIdFromNetwork(params?.inputCurrency?.network)} + chainId={params?.inputCurrency?.chainId} colors={params?.inputCurrency?.colors} theme={theme} /> @@ -652,7 +649,7 @@ export const explainers = (params, theme) => { size={40} icon={params?.inputCurrency?.icon_url} symbol={params?.inputCurrency?.symbol} - chainId={ethereumUtils.getChainIdFromNetwork(params?.inputCurrency?.network)} + chainId={params?.inputCurrency?.chainId} colors={params?.inputCurrency?.colors} theme={theme} /> @@ -661,7 +658,7 @@ export const explainers = (params, theme) => { size={40} icon={params?.outputCurrency?.icon_url} symbol={params?.outputCurrency?.symbol} - chainId={ethereumUtils.getChainIdFromNetwork(params?.outputCurrency?.network)} + chainId={params?.outputCurrency?.chainId} colors={params?.outputCurrency?.colors} theme={theme} /> @@ -678,7 +675,7 @@ export const explainers = (params, theme) => { size={40} icon={params?.inputCurrency?.icon_url} symbol={params?.inputCurrency?.symbol} - chainId={ethereumUtils.getChainIdFromNetwork(params?.inputCurrency?.network)} + chainId={params?.inputCurrency?.chainId} colors={params?.inputCurrency?.colors} theme={theme} /> @@ -687,7 +684,7 @@ export const explainers = (params, theme) => { size={40} icon={params?.outputCurrency?.icon_url} symbol={params?.outputCurrency?.symbol} - chainId={ethereumUtils.getChainIdFromNetwork(params?.outputCurrency?.network)} + chainId={params?.outputCurrency?.chainId} colors={params?.outputCurrency?.colors} theme={theme} /> @@ -697,24 +694,24 @@ export const explainers = (params, theme) => { availableNetworks: { buttonText: `Go to Hop`, extraHeight: -90, - text: availableNetworksExplainer(params?.tokenSymbol, params?.networks), + text: availableNetworksExplainer(params?.tokenSymbol, params?.chainIds), title: - params?.networks?.length > 1 + params?.chainIds?.length > 1 ? lang.t('explain.available_networks.title_plural', { - length: params?.networks?.length, + length: params?.chainIds?.length, }) : lang.t('explain.available_networks.title_singular', { - network: params?.networks?.[0], + network: params?.chainIds?.[0], }), logo: ( - {params?.networks?.map((network, index) => { + {params?.chainIds?.map((chainId, index) => { return ( 0 ? -12 : params?.networks?.length % 2 === 0 ? -2 : -30, + custom: index > 0 ? -12 : params?.chainIds?.length % 2 === 0 ? -2 : -30, }} style={{ borderColor: colors.transparent, @@ -723,10 +720,10 @@ export const explainers = (params, theme) => { zIndex: index, }} width={{ custom: 40 }} - zIndex={params?.networks?.length - index} + zIndex={params?.chainIds?.length - index} > - {network !== 'mainnet' ? ( - + {chainId !== ChainId.mainnet ? ( + ) : ( @@ -779,7 +776,7 @@ export const explainers = (params, theme) => { {lang.t('explain.obtain_l2_asset.fragment3')} ), - logo: , + logo: , title: lang.t('explain.obtain_l2_asset.title', { networkName: params?.networkName, }), @@ -867,19 +864,12 @@ export const explainers = (params, theme) => { }, swap_refuel_add: { logo: ( - + { networkName: params?.networkName, gasToken: params?.gasToken, }), - textColor: colors?.networkColors[params?.network], - bgColor: colors?.networkColors[params?.network] && colors?.alpha(colors?.networkColors[params?.network], 0.05), + textColor: colors?.networkColors[chainId], + bgColor: colors?.networkColors[chainId] && colors?.alpha(colors?.networkColors[chainId], 0.05), onPress: params?.onRefuel, }, }, swap_refuel_deduct: { logo: ( - + { networkName: params?.networkName, gasToken: params?.gasToken, }), - textColor: colors?.networkColors[params?.network], - bgColor: colors?.networkColors[params?.network] && colors?.alpha(colors?.networkColors[params?.network], 0.05), + textColor: colors?.networkColors[chainId], + bgColor: colors?.networkColors[chainId] && colors?.alpha(colors?.networkColors[chainId], 0.05), onPress: params?.onRefuel, }, }, swap_refuel_notice: { extraHeight: 50, logo: ( - + diff --git a/src/screens/MintsSheet/card/Card.tsx b/src/screens/MintsSheet/card/Card.tsx index 1870ce8454e..d9e30975c0a 100644 --- a/src/screens/MintsSheet/card/Card.tsx +++ b/src/screens/MintsSheet/card/Card.tsx @@ -3,8 +3,7 @@ import React, { useEffect, useState } from 'react'; import { getTimeElapsedFromDate } from '../utils'; import { Bleed, Box, Cover, Inline, Inset, Stack, Text, useForegroundColor } from '@/design-system'; import { abbreviateNumber, convertRawAmountToRoundedDecimal } from '@/helpers/utilities'; -import { getNetworkObj } from '@/networks'; -import { getNetworkFromChainId } from '@/utils/ethereumUtils'; +import { getNetworkObject } from '@/networks'; import { ButtonPressAnimation } from '@/components/animations'; import { Placeholder, RecentMintCell } from './RecentMintCell'; import { View } from 'react-native'; @@ -14,7 +13,7 @@ import * as i18n from '@/languages'; import ChainBadge from '@/components/coin-icon/ChainBadge'; import { navigateToMintCollection } from '@/resources/reservoir/mints'; import { EthCoinIcon } from '@/components/coin-icon/EthCoinIcon'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; export const NUM_NFTS = 3; @@ -29,11 +28,9 @@ export function Card({ collection }: { collection: MintableCollection }) { const separatorTertiary = useForegroundColor('separatorTertiary'); const price = convertRawAmountToRoundedDecimal(collection.mintStatus.price, 18, 6); - const currencySymbol = getNetworkObj(getNetworkFromChainId(collection.chainId)).nativeCurrency.symbol; + const currencySymbol = getNetworkObject({ chainId: collection.chainId }).nativeCurrency.symbol; const isFree = !price; - const network = getNetworkFromChainId(collection.chainId); - // update elapsed time every minute if it's less than an hour useEffect(() => { if (timeElapsed && timeElapsed[timeElapsed.length - 1] === 'm') { @@ -106,7 +103,7 @@ export function Card({ collection }: { collection: MintableCollection }) { chainId: collection.chainId, priceInEth: price, }); - navigateToMintCollection(collection.contract, collection.mintStatus.price, network); + navigateToMintCollection(collection.contract, collection.mintStatus.price, collection.chainId); }} style={{ borderRadius: 99, diff --git a/src/screens/NFTOffersSheet/OfferRow.tsx b/src/screens/NFTOffersSheet/OfferRow.tsx index 2fb228536a2..93b64b83c18 100644 --- a/src/screens/NFTOffersSheet/OfferRow.tsx +++ b/src/screens/NFTOffersSheet/OfferRow.tsx @@ -18,6 +18,7 @@ import { Network } from '@/networks/types'; import { useAccountSettings } from '@/hooks'; import RainbowCoinIcon from '@/components/coin-icon/RainbowCoinIcon'; import { ethereumUtils } from '@/utils'; +import { AddressOrEth } from '@/__swaps__/types/assets'; const NFT_SIZE = 50; const MARKETPLACE_ORB_SIZE = 18; @@ -100,7 +101,7 @@ export const OfferRow = ({ offer }: { offer: NftOffer }) => { const bgColor = useBackgroundColor('surfaceSecondaryElevated'); const chainId = ethereumUtils.getChainIdFromNetwork(offer.network as Network); const { data: externalAsset } = useExternalToken({ - address: offer.paymentToken.address, + address: offer.paymentToken.address as AddressOrEth, chainId, currency: nativeCurrency, }); diff --git a/src/screens/NFTSingleOfferSheet/index.tsx b/src/screens/NFTSingleOfferSheet/index.tsx index 7d682f2f143..16869b59a77 100644 --- a/src/screens/NFTSingleOfferSheet/index.tsx +++ b/src/screens/NFTSingleOfferSheet/index.tsx @@ -39,7 +39,7 @@ import { createWalletClient, http } from 'viem'; import { RainbowError, logger } from '@/logger'; import { useTheme } from '@/theme'; -import { Network } from '@/helpers'; +import { Network, ChainId } from '@/networks/types'; import { getNetworkObject } from '@/networks'; import { CardSize } from '@/components/unique-token/CardSize'; import { queryClient } from '@/react-query'; @@ -205,7 +205,7 @@ export function NFTSingleOfferSheet() { let reservoirEstimate = 0; const txs: Transaction[] = []; const fallbackEstimate = - offer.network === Network.mainnet ? ethUnits.mainnet_nft_offer_gas_fee_fallback : ethUnits.l2_nft_offer_gas_fee_fallback; + offerChainId === ChainId.mainnet ? ethUnits.mainnet_nft_offer_gas_fee_fallback : ethUnits.l2_nft_offer_gas_fee_fallback; steps.forEach(step => step.items?.forEach(item => { if (item?.data?.to && item?.data?.from && item?.data?.data) { @@ -247,13 +247,13 @@ export function NFTSingleOfferSheet() { // estimate gas useEffect(() => { if (!isReadOnlyWallet && !isExpired) { - startPollingGasFees(offer?.network as Network); + startPollingGasFees(offerChainId); estimateGas(); } return () => { stopPollingGasFees(); }; - }, [estimateGas, isExpired, isReadOnlyWallet, offer?.network, startPollingGasFees, stopPollingGasFees, updateTxFee]); + }, [estimateGas, isExpired, isReadOnlyWallet, offer.network, offerChainId, startPollingGasFees, stopPollingGasFees, updateTxFee]); const acceptOffer = useCallback(async () => { logger.debug(`[NFTSingleOfferSheet]: Initiating sale of NFT ${offer.nft.contractAddress}:${offer.nft.tokenId}`); @@ -287,7 +287,7 @@ export function NFTSingleOfferSheet() { chain: networkObj, transport: http(networkObj.rpc()), }); - const nonce = await getNextNonce({ address: accountAddress, network: networkObj.value }); + const nonce = await getNextNonce({ address: accountAddress, chainId: networkObj.id }); try { let errorMessage = ''; let didComplete = false; @@ -333,6 +333,7 @@ export function NFTSingleOfferSheet() { nonce: item?.txHashes?.length > 1 ? nonce + 1 : nonce, asset: { ...offer.paymentToken, + chainId: offerChainId, network: offer.network as Network, uniqueId: getUniqueId(offer.paymentToken.address, offerChainId), }, @@ -347,6 +348,7 @@ export function NFTSingleOfferSheet() { asset: { ...offer.paymentToken, network: offer.network as Network, + chainId: offerChainId, uniqueId: getUniqueId(offer.paymentToken.address, offerChainId), }, value: offer.grossAmount.raw, @@ -371,7 +373,7 @@ export function NFTSingleOfferSheet() { addNewTransaction({ transaction: tx, address: accountAddress, - network: offer.network as Network, + chainId: offerChainId, }); txsRef.current.push(tx.hash); } diff --git a/src/screens/SendConfirmationSheet.tsx b/src/screens/SendConfirmationSheet.tsx index 4fc67c9baaf..9d1613e22b5 100644 --- a/src/screens/SendConfirmationSheet.tsx +++ b/src/screens/SendConfirmationSheet.tsx @@ -57,11 +57,11 @@ import { position } from '@/styles'; import { useTheme } from '@/theme'; import { ethereumUtils, getUniqueTokenType, promiseUtils } from '@/utils'; import { logger, RainbowError } from '@/logger'; -import { getNetworkObj } from '@/networks'; import { IS_ANDROID } from '@/env'; import { useConsolidatedTransactions } from '@/resources/transactions/consolidatedTransactions'; import RainbowCoinIcon from '@/components/coin-icon/RainbowCoinIcon'; import { performanceTracking, TimeToSignOperation, Screens } from '@/state/performance/performance'; +import { ChainId, chainIdToNameMapping } from '@/networks/types'; const Container = styled(Centered).attrs({ direction: 'column', @@ -97,12 +97,12 @@ const checkboxOffset = 44; export function getDefaultCheckboxes({ isENS, ensProfile, - network, + chainId, toAddress, }: { isENS: boolean; ensProfile: ENSProfile; - network: string; + chainId: ChainId; toAddress: string; }): Checkbox[] { if (isENS) { @@ -132,7 +132,7 @@ export function getDefaultCheckboxes({ checked: false, id: 'has-wallet-that-supports', label: lang.t('wallet.transaction.checkboxes.has_a_wallet_that_supports', { - networkName: capitalize(network), + networkName: capitalize(chainIdToNameMapping[chainId]), }), }, ]; @@ -195,7 +195,7 @@ export const SendConfirmationSheet = () => { }, []); const { - params: { amountDetails, asset, callback, ensProfile, isL2, isNft, network, to, toAddress }, + params: { amountDetails, asset, callback, ensProfile, isL2, isNft, chainId, to, toAddress }, // eslint-disable-next-line @typescript-eslint/no-explicit-any } = useRoute(); @@ -229,7 +229,7 @@ export const SendConfirmationSheet = () => { transactions.forEach(tx => { if (tx.to?.toLowerCase() === toAddress?.toLowerCase() && tx.from?.toLowerCase() === accountAddress?.toLowerCase()) { sends += 1; - if (tx.network === network) { + if (tx.chainId === chainId) { sendsCurrentNetwork += 1; } } @@ -241,7 +241,7 @@ export const SendConfirmationSheet = () => { } } } - }, [accountAddress, isSendingToUserAccount, network, toAddress, transactions]); + }, [accountAddress, isSendingToUserAccount, chainId, toAddress, transactions]); const contact = useMemo(() => { return contacts?.[toAddress?.toLowerCase()]; @@ -250,7 +250,7 @@ export const SendConfirmationSheet = () => { const uniqueTokenType = getUniqueTokenType(asset); const isENS = uniqueTokenType === 'ENS' && profilesEnabled; - const [checkboxes, setCheckboxes] = useState(getDefaultCheckboxes({ ensProfile, isENS, network, toAddress })); + const [checkboxes, setCheckboxes] = useState(getDefaultCheckboxes({ ensProfile, isENS, chainId, toAddress })); useEffect(() => { if (isENS) { @@ -500,7 +500,7 @@ export const SendConfirmationSheet = () => { badgeYPosition={0} borderRadius={10} imageUrl={imageUrl} - network={asset.network} + chainId={asset?.chainId} showLargeShadow size={50} /> @@ -508,7 +508,7 @@ export const SendConfirmationSheet = () => { { address: toAddress, name: avatarName || address(to, 4, 8), }} - network={network} + chainId={chainId} scaleTo={0.75} > { {/* @ts-expect-error JavaScript component */} { onPress={handleL2DisclaimerPress} prominent customText={i18n.t(i18n.l.expanded_state.asset.l2_disclaimer_send, { - network: getNetworkObj(asset.network).name, + network: chainIdToNameMapping[asset.chainId], })} symbol={asset.symbol} /> diff --git a/src/screens/SendSheet.js b/src/screens/SendSheet.js index fc6acb6f425..3cff2615a5b 100644 --- a/src/screens/SendSheet.js +++ b/src/screens/SendSheet.js @@ -1,5 +1,4 @@ import { useRoute } from '@react-navigation/native'; -import { captureEvent, captureException } from '@sentry/react-native'; import lang from 'i18n-js'; import { isEmpty, isEqual, isString } from 'lodash'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; @@ -23,9 +22,7 @@ import { getProvider, isL2Chain, resolveNameOrAddress, - web3Provider, } from '@/handlers/web3'; -import Network from '@/helpers/networkTypes'; import { checkIsValidAddressOrDomain, checkIsValidAddressOrDomainFormat, isENSAddressFormat } from '@/helpers/validators'; import { prefetchENSAvatar, @@ -66,7 +63,7 @@ import { getNextNonce } from '@/state/nonces'; import { usePersistentDominantColorFromImage } from '@/hooks/usePersistentDominantColorFromImage'; import { performanceTracking, Screens, TimeToSignOperation } from '@/state/performance/performance'; import { REGISTRATION_STEPS } from '@/helpers/ens'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; const sheetHeight = deviceUtils.dimensions.height - (IS_ANDROID ? 30 : 10); const statusBarHeight = IS_IOS ? safeAreaInsetValues.top : StatusBar.currentHeight; @@ -120,7 +117,7 @@ export default function SendSheet(props) { const { contacts, onRemoveContact, filteredContacts } = useContacts(); const { userAccounts, watchedAccounts } = useUserAccounts(); const { sendableUniqueTokens } = useSendableUniqueTokens(); - const { accountAddress, nativeCurrency, network } = useAccountSettings(); + const { accountAddress, nativeCurrency, chainId } = useAccountSettings(); const { isHardwareWallet } = useWallets(); const { action: transferENS } = useENSRegistrationActionHandler({ @@ -267,10 +264,10 @@ export default function SendSheet(props) { // belongs to if (prevChainId !== currentChainId) { InteractionManager.runAfterInteractions(() => { - startPollingGasFees(ethereumUtils.getNetworkFromChainId(currentChainId)); + startPollingGasFees(currentChainId); }); } - }, [startPollingGasFees, selected.network, prevChainId, currentChainId]); + }, [startPollingGasFees, selected.chainId, prevChainId, currentChainId]); // Stop polling when the sheet is unmounted useEffect(() => { @@ -282,21 +279,19 @@ export default function SendSheet(props) { }, [stopPollingGasFees]); useEffect(() => { - const assetChainId = ethereumUtils.getChainIdFromNetwork(selected?.network); - const networkChainId = ethereumUtils.getChainIdFromNetwork(network); + const assetChainId = selected.chainId; if (assetChainId && (assetChainId !== currentChainId || !currentChainId || prevChainId !== currentChainId)) { - let provider = web3Provider; - if (networkChainId === ChainId.goerli) { + if (chainId === ChainId.goerli) { setCurrentChainId(ChainId.goerli); - provider = getProvider({ chainId: ChainId.goerli }); + const provider = getProvider({ chainId: ChainId.goerli }); setCurrentProvider(provider); } else { setCurrentChainId(assetChainId); - provider = getProvider({ chainId: currentChainId }); + const provider = getProvider({ chainId: currentChainId }); setCurrentProvider(provider); } } - }, [currentChainId, isNft, network, prevChainId, selected?.network, sendUpdateSelected]); + }, [currentChainId, isNft, chainId, prevChainId, selected?.chainId, sendUpdateSelected]); const onChangeNativeAmount = useCallback( newNativeAmount => { @@ -424,7 +419,7 @@ export default function SendSheet(props) { }, true, currentProvider, - currentChainIdNetwork + currentChainId ); if (!lessThan(updatedGasLimit, gasLimit)) { @@ -466,7 +461,7 @@ export default function SendSheet(props) { from: accountAddress, gasLimit: gasLimitToUse, network: currentChainIdNetwork, - nonce: nextNonce ?? (await getNextNonce({ address: accountAddress, network: currentChainIdNetwork })), + nonce: nextNonce ?? (await getNextNonce({ address: accountAddress, chainId: currentChainId })), to: toAddress, ...gasParams, }; @@ -515,7 +510,7 @@ export default function SendSheet(props) { txDetails.status = 'pending'; addNewTransaction({ address: accountAddress, - network: currentChainIdNetwork, + chainId: currentChainId, transaction: txDetails, }); } @@ -674,7 +669,7 @@ export default function SendSheet(props) { const checkboxes = getDefaultCheckboxes({ ensProfile, isENS: true, - network, + chainId, toAddress: recipient, }); navigate(Routes.SEND_CONFIRMATION_SHEET, { @@ -686,7 +681,7 @@ export default function SendSheet(props) { isENS, isL2, isNft, - network: ethereumUtils.getNetworkFromChainId(currentChainId), + chainId: currentChainId, profilesEnabled, to: recipient, toAddress, @@ -701,7 +696,7 @@ export default function SendSheet(props) { isNft, nativeCurrencyInputRef, navigate, - network, + chainId, profilesEnabled, recipient, selected, @@ -751,11 +746,11 @@ export default function SendSheet(props) { const [ensSuggestions, setEnsSuggestions] = useState([]); const [loadingEnsSuggestions, setLoadingEnsSuggestions] = useState(false); useEffect(() => { - if (network === Network.mainnet && !recipientOverride && recipient?.length) { + if (chainId === ChainId.mainnet && !recipientOverride && recipient?.length) { setLoadingEnsSuggestions(true); debouncedFetchSuggestions(recipient, setEnsSuggestions, setLoadingEnsSuggestions, profilesEnabled); } - }, [network, recipient, recipientOverride, setEnsSuggestions, watchedAccounts, profilesEnabled]); + }, [chainId, recipient, recipientOverride, setEnsSuggestions, watchedAccounts, profilesEnabled]); useEffect(() => { checkAddress(debouncedInput); @@ -764,7 +759,7 @@ export default function SendSheet(props) { useEffect(() => { if (!currentProvider?._network?.chainId) return; - const assetChainId = ethereumUtils.getChainIdFromNetwork(selected?.network); + const assetChainId = selected.chainId; const currentProviderChainId = currentProvider._network.chainId; if (assetChainId === currentChainId && currentProviderChainId === currentChainId && isValidAddress && !isEmpty(selected)) { @@ -777,7 +772,7 @@ export default function SendSheet(props) { }, false, currentProvider, - ethereumUtils.getNetworkFromChainId(currentChainId) + currentChainId ) .then(async gasLimit => { if (getNetworkObject({ chainId: currentChainId }).gas?.OptimismTxFee) { @@ -801,7 +796,7 @@ export default function SendSheet(props) { toAddress, updateTxFee, updateTxFeeForOptimism, - network, + chainId, isNft, currentChainId, ]); @@ -854,7 +849,6 @@ export default function SendSheet(props) { [name, emoji]); diff --git a/src/screens/SettingsSheet/components/DevSection.tsx b/src/screens/SettingsSheet/components/DevSection.tsx index 44daf6f517f..8821b07b2b8 100644 --- a/src/screens/SettingsSheet/components/DevSection.tsx +++ b/src/screens/SettingsSheet/components/DevSection.tsx @@ -1,12 +1,6 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; import lang from 'i18n-js'; import React, { useCallback, useContext, useState } from 'react'; -import { - // @ts-ignore - HARDHAT_URL_ANDROID, - // @ts-ignore - HARDHAT_URL_IOS, -} from 'react-native-dotenv'; // @ts-ignore import Restart from 'react-native-restart'; import { useDispatch } from 'react-redux'; @@ -16,10 +10,8 @@ import MenuContainer from './MenuContainer'; import MenuItem from './MenuItem'; import { WrappedAlert as Alert } from '@/helpers/alert'; import { deleteAllBackups } from '@/handlers/cloudBackup'; -import { web3SetHttpProvider } from '@/handlers/web3'; import { RainbowContext } from '@/helpers/RainbowContext'; import isTestFlight from '@/helpers/isTestFlight'; -import networkTypes from '@/helpers/networkTypes'; import { useWallets } from '@/hooks'; import { ImgixImage } from '@/components/images'; import { wipeKeychain } from '@/model/keychain'; @@ -48,11 +40,13 @@ import { getFCMToken } from '@/notifications/tokens'; import { removeGlobalNotificationSettings } from '@/notifications/settings/settings'; import { nonceStore } from '@/state/nonces'; import { pendingTransactionsStore } from '@/state/pendingTransactions'; +import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; const DevSection = () => { const { navigate } = useNavigation(); const { config, setConfig } = useContext(RainbowContext) as any; const { wallets } = useWallets(); + const setConnectedToHardhat = useConnectedToHardhatStore.getState().setConnectedToHardhat; const { walletNotificationSettings } = useAllNotificationSettingsFromStorage(); const dispatch = useDispatch(); @@ -75,15 +69,16 @@ const DevSection = () => { const connectToHardhat = useCallback(async () => { try { - const ready = await web3SetHttpProvider((ios && HARDHAT_URL_IOS) || (android && HARDHAT_URL_ANDROID) || 'http://127.0.0.1:8545'); - logger.debug(`[DevSection] connected to hardhat: ${ready}`); + const connectToHardhat = useConnectedToHardhatStore.getState().connectedToHardhat; + setConnectedToHardhat(!connectToHardhat); + logger.debug(`[DevSection] connected to hardhat`); } catch (e) { - await web3SetHttpProvider(networkTypes.mainnet); + setConnectedToHardhat(false); logger.error(new RainbowError(`[DevSection] error connecting to hardhat: ${e}`)); } navigate(Routes.PROFILE_SCREEN); dispatch(explorerInit()); - }, [dispatch, navigate]); + }, [dispatch, navigate, setConnectedToHardhat]); const checkAlert = useCallback(async () => { try { @@ -317,7 +312,15 @@ const DevSection = () => { onPress={connectToHardhat} size={52} testID="hardhat-section" - titleComponent={} + titleComponent={ + + } /> } diff --git a/src/screens/SettingsSheet/components/NetworkSection.tsx b/src/screens/SettingsSheet/components/NetworkSection.tsx index 7e8e32ffa21..b8edc2256cc 100644 --- a/src/screens/SettingsSheet/components/NetworkSection.tsx +++ b/src/screens/SettingsSheet/components/NetworkSection.tsx @@ -9,44 +9,44 @@ import { analytics } from '@/analytics'; import { Separator, Stack } from '@/design-system'; import { useAccountSettings, useInitializeAccountData, useLoadAccountData, useResetAccountState } from '@/hooks'; import { settingsUpdateNetwork } from '@/redux/settings'; -import { Network } from '@/helpers'; -import { RainbowNetworks } from '@/networks'; +import { RainbowNetworkObjects } from '@/networks'; +import { ChainId } from '@/networks/types'; -const networks = values(RainbowNetworks).filter(({ networkType }) => networkType !== 'layer2'); +const networkObjects = values(RainbowNetworkObjects).filter(({ networkType }) => networkType !== 'layer2'); interface NetworkSectionProps { inDevSection?: boolean; } const NetworkSection = ({ inDevSection }: NetworkSectionProps) => { - const { network, testnetsEnabled } = useAccountSettings(); + const { chainId, testnetsEnabled } = useAccountSettings(); const resetAccountState = useResetAccountState(); const loadAccountData = useLoadAccountData(); const initializeAccountData = useInitializeAccountData(); const dispatch = useDispatch(); const onNetworkChange = useCallback( - async (network: Network) => { + async (chainId: ChainId) => { await resetAccountState(); - await dispatch(settingsUpdateNetwork(network)); + await dispatch(settingsUpdateNetwork(chainId)); InteractionManager.runAfterInteractions(async () => { await loadAccountData(); initializeAccountData(); - analytics.track('Changed network', { network }); + analytics.track('Changed network', { chainId }); }); }, [dispatch, initializeAccountData, loadAccountData, resetAccountState] ); const renderNetworkList = useCallback(() => { - return networks.map(({ name, value, networkType }) => ( + return networkObjects.map(({ name, id, networkType }) => ( onNetworkChange(value)} - rightComponent={value === network && } + key={id} + onPress={() => onNetworkChange(id)} + rightComponent={id === chainId && } size={52} - testID={`${value}-network`} + testID={`${id}-network`} titleComponent={ { } /> )); - }, [inDevSection, network, onNetworkChange, testnetsEnabled]); + }, [inDevSection, chainId, onNetworkChange, testnetsEnabled]); return inDevSection ? ( }>{renderNetworkList()} diff --git a/src/screens/SettingsSheet/components/NotificationsSection.tsx b/src/screens/SettingsSheet/components/NotificationsSection.tsx index 6cbc3dee350..1fb1099dd9a 100644 --- a/src/screens/SettingsSheet/components/NotificationsSection.tsx +++ b/src/screens/SettingsSheet/components/NotificationsSection.tsx @@ -17,7 +17,7 @@ import { abbreviations, deviceUtils } from '@/utils'; import { Box } from '@/design-system'; import { removeFirstEmojiFromString, returnStringFirstEmoji } from '@/helpers/emojiHandler'; import { RainbowAccount } from '@/model/wallet'; -import { isTestnetNetwork } from '@/handlers/web3'; +import { isTestnetChain } from '@/handlers/web3'; import { useFocusEffect } from '@react-navigation/native'; import { SettingsLoadingIndicator } from '@/screens/SettingsSheet/components/SettingsLoadingIndicator'; import { showNotificationSubscriptionErrorAlert, showOfflineAlert } from '@/screens/SettingsSheet/components/notificationAlerts'; @@ -157,8 +157,8 @@ const WalletRow = ({ ens, groupOff, isTestnet, loading, notificationSettings, wa const NotificationsSection = () => { const { justBecameActive } = useAppState(); const { navigate } = useNavigation(); - const { network } = useAccountSettings(); - const isTestnet = isTestnetNetwork(network); + const { chainId } = useAccountSettings(); + const isTestnet = isTestnetChain({ chainId }); const { wallets, walletNames } = useWallets(); const { isConnected } = useNetInfo(); const { points_enabled, points_notifications_toggle } = useRemoteConfig(); diff --git a/src/screens/SettingsSheet/components/SettingsSection.tsx b/src/screens/SettingsSheet/components/SettingsSection.tsx index 27c29e18593..9fae44a89eb 100644 --- a/src/screens/SettingsSheet/components/SettingsSection.tsx +++ b/src/screens/SettingsSheet/components/SettingsSection.tsx @@ -203,27 +203,6 @@ const SettingsSection = ({ testID="currency-section" titleComponent={} /> - {/* {(testnetsEnabled || IS_DEV) && ( - - } - onPress={onPressNetwork} - rightComponent={ - - {getNetworkObj(network).name} - - } - size={60} - testID="network-section" - titleComponent={ - - } - /> - )} */} void; onCancel: (error?: Error) => void; onCloseScreen: (canceled: boolean) => void; - network: Network; chainId: ChainId; address: string; source: RequestSource; }; -const SCREEN_FOR_REQUEST_SOURCE = { - browser: Screens.DAPP_BROWSER, - walletconnect: Screens.WALLETCONNECT, -}; - export type SignTransactionSheetRouteProp = RouteProp<{ SignTransactionSheet: SignTransactionSheetParams }, 'SignTransactionSheet'>; export const SignTransactionSheet = () => { - const { goBack, navigate } = useNavigation(); + const { goBack } = useNavigation(); const { colors, isDarkMode } = useTheme(); const { accountAddress, nativeCurrency } = useAccountSettings(); - const [simulationData, setSimulationData] = useState(); - const [simulationError, setSimulationError] = useState(undefined); - const [simulationScanResult, setSimulationScanResult] = useState(undefined); const { params: routeParams } = useRoute(); const { wallets, walletNames } = useWallets(); @@ -171,28 +98,24 @@ export const SignTransactionSheet = () => { onSuccess: onSuccessCallback, onCancel: onCancelCallback, onCloseScreen: onCloseScreenCallback, - chainId: currentChainId, - address: currentAddress, + chainId, + address: specifiedAddress, // for request type specific handling source, } = routeParams; - const isMessageRequest = isMessageDisplayType(transactionDetails.payload.method); + console.log({ specifiedAddress }); + + const addressToUse = specifiedAddress ?? accountAddress; - const isPersonalSign = checkIsPersonalSign(transactionDetails.payload.method); + const { provider, nativeAsset } = useProviderSetup(chainId, addressToUse); + + const isMessageRequest = isMessageDisplayType(transactionDetails.payload.method); + const isPersonalSignRequest = isPersonalSign(transactionDetails.payload.method); const label = useForegroundColor('label'); const surfacePrimary = useBackgroundColor('surfacePrimary'); - const [provider, setProvider] = useState(null); - const [isAuthorizing, setIsAuthorizing] = useState(false); - const [isLoading, setIsLoading] = useState(!isPersonalSign); - const [methodName, setMethodName] = useState(null); - const calculatingGasLimit = useRef(false); - const [isBalanceEnough, setIsBalanceEnough] = useState(); - const [nonceForDisplay, setNonceForDisplay] = useState(); - - const [nativeAsset, setNativeAsset] = useState(null); const formattedDappUrl = useMemo(() => { try { const { hostname } = new URL(transactionDetails?.dappUrl); @@ -202,107 +125,16 @@ export const SignTransactionSheet = () => { } }, [transactionDetails]); - const { - gasLimit, - isValidGas, - startPollingGasFees, - stopPollingGasFees, - isSufficientGas, - updateTxFee, - selectedGasFee, - gasFeeParamsBySpeed, - } = useGas(); - - const simulationUnavailable = isPersonalSign; - - const itemCount = (simulationData?.in?.length || 0) + (simulationData?.out?.length || 0) + (simulationData?.approvals?.length || 0); - - const noChanges = !!(simulationData && itemCount === 0) && simulationScanResult === TransactionScanResultType.Ok; - const req = transactionDetails?.payload?.params?.[0]; const request = useMemo(() => { return isMessageRequest - ? { message: transactionDetails?.displayDetails?.request } + ? { message: transactionDetails?.displayDetails?.request || '' } : { ...transactionDetails?.displayDetails?.request, - nativeAsset: nativeAsset, + nativeAsset, }; }, [isMessageRequest, transactionDetails?.displayDetails?.request, nativeAsset]); - const calculateGasLimit = useCallback(async () => { - calculatingGasLimit.current = true; - const txPayload = req; - if (isHex(txPayload?.type)) { - txPayload.type = hexToNumber(txPayload?.type); - } - // use the default - let gas = txPayload.gasLimit || txPayload.gas; - - const provider = getProvider({ chainId: currentChainId }); - try { - // attempt to re-run estimation - logger.debug('[SignTransactionSheet]: Estimating gas limit', { gas }, logger.DebugContext.walletconnect); - // safety precaution: we want to ensure these properties are not used for gas estimation - const cleanTxPayload = omitFlatten(txPayload, ['gas', 'gasLimit', 'gasPrice', 'maxFeePerGas', 'maxPriorityFeePerGas']); - const rawGasLimit = await estimateGas(cleanTxPayload, provider); - logger.debug('[SignTransactionSheet]: Estimated gas limit', { rawGasLimit }, logger.DebugContext.walletconnect); - if (rawGasLimit) { - gas = toHex(rawGasLimit); - } - } catch (error) { - logger.error(new RainbowError('[SignTransactionSheet]: error estimating gas'), { error }); - } finally { - logger.debug('[SignTransactionSheet]: Setting gas limit to', { gas: convertHexToString(gas) }, logger.DebugContext.walletconnect); - const networkObject = getNetworkObject({ chainId: currentChainId }); - if (networkObject && networkObject.gas.OptimismTxFee) { - const l1GasFeeOptimism = await ethereumUtils.calculateL1FeeOptimism(txPayload, provider); - updateTxFee(gas, null, l1GasFeeOptimism); - } else { - updateTxFee(gas, null); - } - } - }, [currentChainId, req, updateTxFee]); - - const fetchMethodName = useCallback( - async (data: string) => { - const methodSignaturePrefix = data.substr(0, 10); - try { - const { name } = await methodRegistryLookupAndParse(methodSignaturePrefix, currentChainId); - if (name) { - setMethodName(name); - } - } catch (e) { - setMethodName(data); - } - }, - [currentChainId] - ); - - // start polling for gas and get fn name - useEffect(() => { - InteractionManager.runAfterInteractions(() => { - if (currentChainId) { - if (!isMessageRequest) { - const network = ethereumUtils.getNetworkFromChainId(currentChainId); - startPollingGasFees(network); - fetchMethodName(transactionDetails?.payload?.params[0].data); - } else { - setMethodName(i18n.t(i18n.l.wallet.message_signing.request)); - } - analytics.track(event.txRequestShownSheet), { source }; - } - }); - }, [isMessageRequest, startPollingGasFees, fetchMethodName, transactionDetails?.payload?.params, source, currentChainId]); - - // get gas limit - useEffect(() => { - if (!isEmpty(gasFeeParamsBySpeed) && !calculatingGasLimit.current && !isMessageRequest && provider) { - InteractionManager.runAfterInteractions(() => { - calculateGasLimit(); - }); - } - }, [calculateGasLimit, gasLimit, gasFeeParamsBySpeed, isMessageRequest, provider, updateTxFee]); - const walletBalance = useMemo(() => { return { amount: nativeAsset?.balance?.amount || 0, @@ -312,173 +144,77 @@ export const SignTransactionSheet = () => { }; }, [nativeAsset?.balance?.amount, nativeAsset?.balance?.display, nativeAsset?.symbol]); - // check native balance is sufficient - useEffect(() => { - if (isMessageRequest) { - setIsBalanceEnough(true); - return; - } + const { gasLimit, isValidGas, startPollingGasFees, stopPollingGasFees, updateTxFee, selectedGasFee, gasFeeParamsBySpeed } = useGas(); - const { gasFee } = selectedGasFee; - if (!walletBalance?.isLoaded || !currentChainId || !gasFee?.estimatedFee) { - return; - } + const { methodName } = useTransactionSetup({ + chainId, + startPollingGasFees, + stopPollingGasFees, + isMessageRequest, + transactionDetails, + source, + }); - // Get the TX fee Amount - const txFeeAmount = fromWei(gasFee?.maxFee?.value?.amount ?? 0); + const { isBalanceEnough } = useHasEnoughBalance({ + isMessageRequest, + walletBalance, + chainId, + selectedGasFee, + req, + }); + + useCalculateGasLimit({ + isMessageRequest, + gasFeeParamsBySpeed, + provider, + req, + updateTxFee, + chainId, + }); - // Get the ETH balance - const balanceAmount = walletBalance?.amount ?? 0; + const { nonceForDisplay } = useNonceForDisplay({ + isMessageRequest, + address: addressToUse, + chainId, + }); - // Get the TX value - const txPayload = req; - const value = txPayload?.value ?? 0; + const { + data: simulationResult, + isLoading: txSimulationLoading, + error: txSimulationApiError, + } = useSimulation( + { + address: addressToUse, + chainId, + isMessageRequest, + nativeCurrency, + req, + requestMessage: request.message, + simulationUnavailable: isPersonalSignRequest, + transactionDetails, + }, + { + enabled: !isPersonalSignRequest, + } + ); - // Check that there's enough ETH to pay for everything! - const totalAmount = new BigNumber(fromWei(value)).plus(txFeeAmount); - const isEnough = greaterThanOrEqualTo(balanceAmount, totalAmount); + const itemCount = + (simulationResult?.simulationData?.in?.length || 0) + + (simulationResult?.simulationData?.out?.length || 0) + + (simulationResult?.simulationData?.approvals?.length || 0); - setIsBalanceEnough(isEnough); - }, [isMessageRequest, isSufficientGas, selectedGasFee, walletBalance, req, currentChainId]); + const noChanges = + !!(simulationResult?.simulationData && itemCount === 0) && simulationResult?.simulationScanResult === TransactionScanResultType.Ok; const accountInfo = useMemo(() => { - const selectedWallet = wallets ? findWalletWithAccount(wallets, currentAddress) : undefined; - const profileInfo = getAccountProfileInfo(selectedWallet, walletNames, currentAddress); + const selectedWallet = wallets ? findWalletWithAccount(wallets, addressToUse) : undefined; + const profileInfo = getAccountProfileInfo(selectedWallet, walletNames, addressToUse); return { ...profileInfo, - address: currentAddress, + address: addressToUse, isHardwareWallet: !!selectedWallet?.deviceId, }; - }, [wallets, currentAddress, walletNames]); - - useEffect(() => { - const initProvider = async () => { - let p; - // check on this o.O - if (currentChainId === ChainId.mainnet) { - p = await getFlashbotsProvider(); - } else { - p = getProvider({ chainId: currentChainId }); - } - - setProvider(p); - }; - initProvider(); - }, [currentChainId, setProvider]); - - useEffect(() => { - (async () => { - const asset = await ethereumUtils.getNativeAssetForNetwork(currentChainId, accountInfo.address); - if (asset && provider) { - const balance = await getOnchainAssetBalance( - asset, - accountInfo.address, - ethereumUtils.getNetworkFromChainId(currentChainId), - provider - ); - if (balance) { - const assetWithOnchainBalance: ParsedAddressAsset = { ...asset, balance }; - setNativeAsset(assetWithOnchainBalance); - } else { - setNativeAsset(asset); - } - } - })(); - }, [accountInfo.address, currentChainId, provider]); - - useEffect(() => { - (async () => { - if (!isMessageRequest && !nonceForDisplay) { - try { - const nonce = await getNextNonce({ address: currentAddress, network: ethereumUtils.getNetworkFromChainId(currentChainId) }); - if (nonce || nonce === 0) { - const nonceAsString = nonce.toString(); - setNonceForDisplay(nonceAsString); - } - } catch (error) { - console.error('Failed to get nonce for display:', error); - } - } - })(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [accountInfo.address, currentChainId, getNextNonce, isMessageRequest]); - - useEffect(() => { - const timeout = setTimeout(async () => { - try { - let simulationData; - if (isMessageRequest) { - // Message Signing - simulationData = await metadataPOSTClient.simulateMessage({ - address: accountAddress, - chainId: currentChainId, - message: { - method: transactionDetails?.payload?.method, - params: [request.message], - }, - domain: transactionDetails?.dappUrl, - }); - // Handle message simulation response - if (isNil(simulationData?.simulateMessage?.simulation) && isNil(simulationData?.simulateMessage?.error)) { - setSimulationData({ in: [], out: [], approvals: [] }); - setSimulationScanResult(simulationData?.simulateMessage?.scanning?.result); - } else if (simulationData?.simulateMessage?.error && !simulationUnavailable) { - setSimulationError(simulationData?.simulateMessage?.error?.type); - setSimulationScanResult(simulationData?.simulateMessage?.scanning?.result); - setSimulationData(undefined); - } else if (simulationData.simulateMessage?.simulation && !simulationUnavailable) { - setSimulationData(simulationData.simulateMessage?.simulation); - setSimulationScanResult(simulationData?.simulateMessage?.scanning?.result); - } - } else { - // TX Signing - simulationData = await metadataPOSTClient.simulateTransactions({ - chainId: currentChainId, - currency: nativeCurrency?.toLowerCase(), - transactions: [ - { - from: req?.from, - to: req?.to, - data: req?.data, - value: req?.value || '0x0', - }, - ], - domain: transactionDetails?.dappUrl, - }); - // Handle TX simulation response - if (isNil(simulationData?.simulateTransactions?.[0]?.simulation) && isNil(simulationData?.simulateTransactions?.[0]?.error)) { - setSimulationData({ in: [], out: [], approvals: [] }); - setSimulationScanResult(simulationData?.simulateTransactions?.[0]?.scanning?.result); - } else if (simulationData?.simulateTransactions?.[0]?.error) { - setSimulationError(simulationData?.simulateTransactions?.[0]?.error?.type); - setSimulationData(undefined); - setSimulationScanResult(simulationData?.simulateTransactions[0]?.scanning?.result); - } else if (simulationData.simulateTransactions?.[0]?.simulation) { - setSimulationData(simulationData.simulateTransactions[0]?.simulation); - setSimulationScanResult(simulationData?.simulateTransactions[0]?.scanning?.result); - } - } - } catch (error) { - logger.error(new RainbowError('[SignTransactionSheet]: Error while simulating'), { error }); - } finally { - setIsLoading(false); - } - }, 750); - - return () => { - clearTimeout(timeout); - }; - }, [ - accountAddress, - currentChainId, - isMessageRequest, - isPersonalSign, - nativeCurrency, - req, - request.message, - simulationUnavailable, - transactionDetails, - ]); + }, [wallets, addressToUse, walletNames]); const closeScreen = useCallback( (canceled: boolean) => @@ -526,86 +262,12 @@ export const SignTransactionSheet = () => { [accountInfo.isHardwareWallet, closeScreen, onCancelCallback, source, transactionDetails?.payload?.method] ); - const handleSignMessage = useCallback(async () => { - const message = transactionDetails?.payload?.params.find((p: string) => !isAddress(p)); - let response = null; - - const provider = getProvider({ chainId: currentChainId }); - if (!provider) { - return; - } - - const existingWallet = await performanceTracking.getState().executeFn({ - fn: loadWallet, - screen: SCREEN_FOR_REQUEST_SOURCE[source], - operation: TimeToSignOperation.KeychainRead, - })({ - address: accountInfo.address, - provider, - timeTracking: { - screen: SCREEN_FOR_REQUEST_SOURCE[source], - operation: TimeToSignOperation.Authentication, - }, - }); - - if (!existingWallet) { - return; - } - switch (transactionDetails?.payload?.method) { - case PERSONAL_SIGN: - response = await performanceTracking.getState().executeFn({ - fn: signPersonalMessage, - screen: SCREEN_FOR_REQUEST_SOURCE[source], - operation: TimeToSignOperation.SignTransaction, - })(message, existingWallet); - break; - case SIGN_TYPED_DATA_V4: - case SIGN_TYPED_DATA: - response = await performanceTracking.getState().executeFn({ - fn: signTypedDataMessage, - screen: SCREEN_FOR_REQUEST_SOURCE[source], - operation: TimeToSignOperation.SignTransaction, - })(message, existingWallet); - break; - default: - break; - } - - if (response?.result) { - analytics.track(event.txRequestApprove, { - source, - requestType: 'signature', - dappName: transactionDetails?.dappName, - dappUrl: transactionDetails?.dappUrl, - isHardwareWallet: accountInfo.isHardwareWallet, - network: ethereumUtils.getNetworkFromChainId(currentChainId), - }); - onSuccessCallback?.(response.result); - - closeScreen(false); - } else { - await onCancel(response?.error); - } - }, [ - transactionDetails?.payload?.params, - transactionDetails?.payload?.method, - transactionDetails?.dappName, - transactionDetails?.dappUrl, - currentChainId, - accountInfo.address, - accountInfo.isHardwareWallet, - source, - onSuccessCallback, - closeScreen, - onCancel, - ]); - const handleConfirmTransaction = useCallback(async () => { const sendInsteadOfSign = transactionDetails.payload.method === SEND_TRANSACTION; const txPayload = req; let { gas } = txPayload; const gasLimitFromPayload = txPayload?.gasLimit; - if (!currentChainId) return; + if (!chainId) return; try { logger.debug( '[SignTransactionSheet]: gas suggested by dapp', @@ -644,7 +306,7 @@ export const SignTransactionSheet = () => { const gasParams = parseGasParamsForTransaction(selectedGasFee); const calculatedGasLimit = gas || gasLimitFromPayload || gasLimit; - const nonce = await getNextNonce({ address: accountInfo.address, network: ethereumUtils.getNetworkFromChainId(currentChainId) }); + const nonce = await getNextNonce({ address: accountInfo.address, chainId }); let txPayloadUpdated = { ...cleanTxPayload, ...gasParams, @@ -660,11 +322,11 @@ export const SignTransactionSheet = () => { let response = null; try { - if (!currentChainId) { + if (!chainId) { return; } - const provider = getProvider({ chainId: currentChainId }); - if (!provider) { + const providerToUse = provider || getProvider({ chainId }); + if (!providerToUse) { return; } const existingWallet = await performanceTracking.getState().executeFn({ @@ -673,7 +335,7 @@ export const SignTransactionSheet = () => { operation: TimeToSignOperation.KeychainRead, })({ address: accountInfo.address, - provider, + provider: providerToUse, timeTracking: { screen: SCREEN_FOR_REQUEST_SOURCE[source], operation: TimeToSignOperation.Authentication, @@ -691,8 +353,8 @@ export const SignTransactionSheet = () => { screen: SCREEN_FOR_REQUEST_SOURCE[source], operation: TimeToSignOperation.BroadcastTransaction, })({ - existingWallet: existingWallet, - provider, + existingWallet, + provider: providerToUse, transaction: txPayloadUpdated, }); } else { @@ -702,7 +364,7 @@ export const SignTransactionSheet = () => { operation: TimeToSignOperation.SignTransaction, })({ existingWallet, - provider, + provider: providerToUse, transaction: txPayloadUpdated, }); } @@ -720,7 +382,7 @@ export const SignTransactionSheet = () => { if (sendInsteadOfSign && sendResult?.hash) { txDetails = { status: 'pending', - chainId: currentChainId, + chainId, asset: displayDetails?.request?.asset || nativeAsset, contract: { name: transactionDetails.dappName, @@ -730,18 +392,18 @@ export const SignTransactionSheet = () => { from: displayDetails?.request?.from, gasLimit, hash: sendResult.hash, - network: ethereumUtils.getNetworkFromChainId(currentChainId) || Network.mainnet, + network: getNetworkObject({ chainId }).value, nonce: sendResult.nonce, to: displayDetails?.request?.to, value: sendResult.value.toString(), type: 'contract_interaction', ...gasParams, }; - if (accountAddress?.toLowerCase() === txDetails.from?.toLowerCase()) { + if (accountInfo.address?.toLowerCase() === txDetails.from?.toLowerCase()) { addNewTransaction({ transaction: txDetails, - network: ethereumUtils.getNetworkFromChainId(currentChainId) || Network.mainnet, - address: accountAddress, + chainId, + address: accountInfo.address, }); txSavedInCurrentWallet = true; } @@ -752,7 +414,7 @@ export const SignTransactionSheet = () => { dappName: transactionDetails.dappName, dappUrl: transactionDetails.dappUrl, isHardwareWallet: accountInfo.isHardwareWallet, - network: ethereumUtils.getNetworkFromChainId(currentChainId), + network: getNetworkObject({ chainId }).value, }); if (!sendInsteadOfSign) { @@ -772,7 +434,7 @@ export const SignTransactionSheet = () => { await switchToWalletWithAddress(txDetails?.from as string); addNewTransaction({ transaction: txDetails as NewTransaction, - network: ethereumUtils.getNetworkFromChainId(currentChainId) || Network.mainnet, + chainId, address: txDetails?.from as string, }); }); @@ -783,7 +445,7 @@ export const SignTransactionSheet = () => { dappUrl: transactionDetails?.dappUrl, formattedDappUrl, rpcMethod: req?.method, - network: ethereumUtils.getNetworkFromChainId(currentChainId), + network: getNetworkObject({ chainId }).value, }); // If the user is using a hardware wallet, we don't want to close the sheet on an error if (!accountInfo.isHardwareWallet) { @@ -797,7 +459,7 @@ export const SignTransactionSheet = () => { transactionDetails.dappUrl, transactionDetails.imageUrl, req, - currentChainId, + chainId, selectedGasFee, gasLimit, accountInfo.address, @@ -806,57 +468,106 @@ export const SignTransactionSheet = () => { source, closeScreen, nativeAsset, - accountAddress, onSuccessCallback, switchToWalletWithAddress, formattedDappUrl, onCancel, ]); - const onConfirm = useCallback(async () => { - if (isMessageRequest) { - return handleSignMessage(); - } - if (!isBalanceEnough || !isValidGas) return; - return handleConfirmTransaction(); - }, [handleConfirmTransaction, handleSignMessage, isBalanceEnough, isMessageRequest, isValidGas]); + const handleSignMessage = useCallback(async () => { + const message = transactionDetails?.payload?.params.find((p: string) => !isAddress(p)); + let response = null; - const onPressSend = useCallback(async () => { - if (isAuthorizing) return; - setIsAuthorizing(true); - try { - await onConfirm(); - setIsAuthorizing(false); - } catch (error) { - setIsAuthorizing(false); + const providerToUse = provider || getProvider({ chainId }); + if (!providerToUse) { + return; } - }, [isAuthorizing, onConfirm]); - const submitFn = useCallback( - () => - performanceTracking.getState().executeFn({ - fn: async () => { - if (!isBalanceEnough) { - navigate(Routes.ADD_CASH_SHEET); - return; - } - if (accountInfo.isHardwareWallet) { - navigate(Routes.HARDWARE_WALLET_TX_NAVIGATOR, { submit: onPressSend }); - } else { - await onPressSend(); - } - }, - operation: TimeToSignOperation.CallToAction, + const existingWallet = await performanceTracking.getState().executeFn({ + fn: loadWallet, + screen: SCREEN_FOR_REQUEST_SOURCE[source], + operation: TimeToSignOperation.KeychainRead, + })({ + address: accountInfo.address, + provider: providerToUse, + timeTracking: { screen: SCREEN_FOR_REQUEST_SOURCE[source], - })(), - [accountInfo.isHardwareWallet, isBalanceEnough, navigate, onPressSend, source] - ); + operation: TimeToSignOperation.Authentication, + }, + }); + + if (!existingWallet) { + return; + } + switch (transactionDetails?.payload?.method) { + case PERSONAL_SIGN: + response = await performanceTracking.getState().executeFn({ + fn: signPersonalMessage, + screen: SCREEN_FOR_REQUEST_SOURCE[source], + operation: TimeToSignOperation.SignTransaction, + })(message, existingWallet); + break; + case SIGN_TYPED_DATA_V4: + case SIGN_TYPED_DATA: + response = await performanceTracking.getState().executeFn({ + fn: signTypedDataMessage, + screen: SCREEN_FOR_REQUEST_SOURCE[source], + operation: TimeToSignOperation.SignTransaction, + })(message, existingWallet); + break; + default: + break; + } + + if (response?.result) { + analytics.track(event.txRequestApprove, { + source, + requestType: 'signature', + dappName: transactionDetails?.dappName, + dappUrl: transactionDetails?.dappUrl, + isHardwareWallet: accountInfo.isHardwareWallet, + network: getNetworkObject({ chainId }).value, + }); + onSuccessCallback?.(response.result); + + closeScreen(false); + } else { + await onCancel(response?.error); + } + }, [ + transactionDetails?.payload?.params, + transactionDetails?.payload?.method, + transactionDetails?.dappName, + transactionDetails?.dappUrl, + chainId, + accountInfo.address, + accountInfo.isHardwareWallet, + source, + onSuccessCallback, + closeScreen, + onCancel, + ]); + + const { onConfirm } = useConfirmTransaction({ + isMessageRequest, + isBalanceEnough, + isValidGas, + handleSignMessage, + handleConfirmTransaction, + }); + + const { submitFn } = useTransactionSubmission({ + isBalanceEnough, + accountInfo, + onConfirm, + source, + }); const onPressCancel = useCallback(() => onCancel(), [onCancel]); const expandedCardBottomInset = EXPANDED_CARD_BOTTOM_INSET + (isMessageRequest ? 0 : GAS_BUTTON_SPACE); - const canPressConfirm = isMessageRequest || (!!walletBalance?.isLoaded && !!currentChainId && !!selectedGasFee?.gasFee?.estimatedFee); + const canPressConfirm = isMessageRequest || (!!walletBalance?.isLoaded && !!chainId && !!selectedGasFee?.gasFee?.estimatedFee); return ( @@ -905,8 +616,9 @@ export const SignTransactionSheet = () => { { > {transactionDetails.dappName} - {source === 'browser' && } + {source === RequestSource.BROWSER && } {isMessageRequest @@ -927,33 +639,36 @@ export const SignTransactionSheet = () => { - {isMessageRequest ? ( - ) : ( - { ) : ( - {!!currentChainId && walletBalance?.isLoaded && ( + {!!chainId && walletBalance?.isLoaded && ( - + {`${walletBalance?.display} ${i18n.t(i18n.l.walletconnect.simulation.profile_section.on_network, { - network: getNetworkObject({ chainId: currentChainId })?.name, + network: getNetworkObject({ chainId })?.name, })}`} @@ -1024,7 +739,7 @@ export const SignTransactionSheet = () => { /> { disabled={!canPressConfirm} size="big" weight="heavy" - // eslint-disable-next-line react/jsx-props-no-spreading - {...((simulationError || (simulationScanResult && simulationScanResult !== TransactionScanResultType.Ok)) && { - color: simulationScanResult === TransactionScanResultType.Warning ? 'orange' : colors.red, - })} + color={ + simulationResult?.simulationError || + (simulationResult?.simulationScanResult && simulationResult?.simulationScanResult !== TransactionScanResultType.Ok) + ? simulationResult?.simulationScanResult === TransactionScanResultType.Warning + ? 'orange' + : colors.red + : undefined + } /> @@ -1049,7 +768,7 @@ export const SignTransactionSheet = () => { )} - {source === 'browser' && ( + {source === RequestSource.BROWSER && ( { { ); }; - -interface SimulationCardProps { - currentNetwork: Network; - expandedCardBottomInset: number; - isBalanceEnough: boolean | undefined; - isLoading: boolean; - isPersonalSign: boolean; - noChanges: boolean; - simulation: TransactionSimulationResult | undefined; - simulationError: TransactionErrorType | undefined; - simulationScanResult: TransactionScanResultType | undefined; - walletBalance: { - amount: string | number; - display: string; - isLoaded: boolean; - symbol: string; - }; -} - -const SimulationCard = ({ - currentNetwork, - expandedCardBottomInset, - isBalanceEnough, - isLoading, - isPersonalSign, - noChanges, - simulation, - simulationError, - simulationScanResult, - walletBalance, -}: SimulationCardProps) => { - const cardHeight = useSharedValue(COLLAPSED_CARD_HEIGHT); - const contentHeight = useSharedValue(COLLAPSED_CARD_HEIGHT - CARD_BORDER_WIDTH * 2); - const spinnerRotation = useSharedValue(0); - - const simulationUnavailable = isPersonalSign; - - const listStyle = useAnimatedStyle(() => ({ - opacity: noChanges - ? withTiming(1, timingConfig) - : interpolate( - cardHeight.value, - [ - COLLAPSED_CARD_HEIGHT, - contentHeight.value + CARD_BORDER_WIDTH * 2 > MAX_CARD_HEIGHT ? MAX_CARD_HEIGHT : contentHeight.value + CARD_BORDER_WIDTH * 2, - ], - [0, 1] - ), - })); - - const spinnerStyle = useAnimatedStyle(() => { - return { - transform: [{ rotate: `${spinnerRotation.value}deg` }], - }; - }); - - useAnimatedReaction( - () => ({ isLoading, simulationUnavailable }), - ({ isLoading, simulationUnavailable }, previous = { isLoading: false, simulationUnavailable: false }) => { - if (isLoading && !previous?.isLoading) { - spinnerRotation.value = withRepeat(withTiming(360, rotationConfig), -1, false); - } else if ( - (!isLoading && previous?.isLoading) || - (simulationUnavailable && !previous?.simulationUnavailable && previous?.isLoading) - ) { - spinnerRotation.value = withTiming(360, timingConfig); - } - }, - [isLoading, simulationUnavailable] - ); - const renderSimulationEventRows = useMemo(() => { - if (isBalanceEnough === false) return null; - - return ( - <> - {simulation?.approvals?.map(change => { - return ( - - ); - })} - {simulation?.out?.map(change => { - return ( - - ); - })} - {simulation?.in?.map(change => { - return ( - - ); - })} - - ); - }, [isBalanceEnough, simulation]); - - const titleColor: TextColor = useMemo(() => { - if (isLoading) { - return 'label'; - } - if (isBalanceEnough === false) { - return 'blue'; - } - if (noChanges || simulationUnavailable) { - return 'labelQuaternary'; - } - if (simulationScanResult === TransactionScanResultType.Warning) { - return 'orange'; - } - if (simulationError || simulationScanResult === TransactionScanResultType.Malicious) { - return 'red'; - } - return 'label'; - }, [isBalanceEnough, isLoading, noChanges, simulationError, simulationScanResult, simulationUnavailable]); - - const titleText = useMemo(() => { - if (isLoading) { - return i18n.t(i18n.l.walletconnect.simulation.simulation_card.titles.simulating); - } - if (isBalanceEnough === false) { - return i18n.t(i18n.l.walletconnect.simulation.simulation_card.titles.not_enough_native_balance, { symbol: walletBalance?.symbol }); - } - if (simulationUnavailable) { - return i18n.t(i18n.l.walletconnect.simulation.simulation_card.titles.simulation_unavailable); - } - if (simulationScanResult === TransactionScanResultType.Warning) { - return i18n.t(i18n.l.walletconnect.simulation.simulation_card.titles.proceed_carefully); - } - if (simulationScanResult === TransactionScanResultType.Malicious) { - return i18n.t(i18n.l.walletconnect.simulation.simulation_card.titles.suspicious_transaction); - } - if (noChanges) { - return i18n.t(i18n.l.walletconnect.simulation.simulation_card.messages.no_changes); - } - if (simulationError) { - return i18n.t(i18n.l.walletconnect.simulation.simulation_card.titles.likely_to_fail); - } - return i18n.t(i18n.l.walletconnect.simulation.simulation_card.titles.simulation_result); - }, [isBalanceEnough, isLoading, noChanges, simulationError, simulationScanResult, simulationUnavailable, walletBalance?.symbol]); - - const isExpanded = useMemo(() => { - if (isLoading || isPersonalSign) { - return false; - } - const shouldExpandOnLoad = isBalanceEnough === false || (!isEmpty(simulation) && !noChanges) || !!simulationError; - return shouldExpandOnLoad; - }, [isBalanceEnough, isLoading, isPersonalSign, noChanges, simulation, simulationError]); - - return ( - - - - - {!isLoading && (simulationError || isBalanceEnough === false || simulationScanResult !== TransactionScanResultType.Ok) ? ( - - ) : ( - - {!isLoading && noChanges && !simulationUnavailable ? ( - - {/* The extra space avoids icon clipping */} - {'􀻾 '} - - ) : ( - - - 􀬨 - - - )} - - )} - - {titleText} - - - {/* TODO: Unhide once we add explainer sheets */} - {/* - - - - - 􀁜 - - - - - */} - - - - {isBalanceEnough === false ? ( - - {i18n.t(i18n.l.walletconnect.simulation.simulation_card.messages.need_more_native, { - symbol: walletBalance?.symbol, - network: getNetworkObj(currentNetwork).name, - })} - - ) : ( - <> - {simulationUnavailable && isPersonalSign && ( - - - {i18n.t(i18n.l.walletconnect.simulation.simulation_card.messages.unavailable_personal_sign)} - - - )} - {simulationError && ( - - {i18n.t(i18n.l.walletconnect.simulation.simulation_card.messages.failed_to_simulate)} - - )} - {simulationScanResult === TransactionScanResultType.Warning && ( - - {i18n.t(i18n.l.walletconnect.simulation.simulation_card.messages.warning)}{' '} - - )} - {simulationScanResult === TransactionScanResultType.Malicious && ( - - {i18n.t(i18n.l.walletconnect.simulation.simulation_card.messages.malicious)} - - )} - - )} - {renderSimulationEventRows} - - - - - ); -}; - -interface DetailsCardProps { - currentNetwork: Network; - expandedCardBottomInset: number; - isBalanceEnough: boolean | undefined; - isLoading: boolean; - meta: TransactionSimulationMeta | undefined; - methodName: string; - noChanges: boolean; - nonce: string | undefined; - toAddress: string; -} - -const DetailsCard = ({ - currentNetwork, - expandedCardBottomInset, - isBalanceEnough, - isLoading, - meta, - methodName, - noChanges, - nonce, - toAddress, -}: DetailsCardProps) => { - const cardHeight = useSharedValue(COLLAPSED_CARD_HEIGHT); - const contentHeight = useSharedValue(COLLAPSED_CARD_HEIGHT - CARD_BORDER_WIDTH * 2); - const [isExpanded, setIsExpanded] = useState(false); - - const listStyle = useAnimatedStyle(() => ({ - opacity: interpolate( - cardHeight.value, - [ - COLLAPSED_CARD_HEIGHT, - contentHeight.value + CARD_BORDER_WIDTH * 2 > MAX_CARD_HEIGHT ? MAX_CARD_HEIGHT : contentHeight.value + CARD_BORDER_WIDTH * 2, - ], - [0, 1] - ), - })); - - const collapsedTextColor: TextColor = isLoading ? 'labelQuaternary' : 'blue'; - - const showFunctionRow = meta?.to?.function || (methodName && methodName.substring(0, 2) !== '0x'); - const isContract = showFunctionRow || meta?.to?.created || meta?.to?.sourceCodeStatus; - const showTransferToRow = !!meta?.transferTo?.address; - // Hide DetailsCard if balance is insufficient once loaded - if (!isLoading && isBalanceEnough === false) { - return <>; - } - return ( - setIsExpanded(true)} - > - - - - - - 􁙠 - - - - {i18n.t(i18n.l.walletconnect.simulation.details_card.title)} - - - - - - {} - {!!(meta?.to?.address || toAddress || showTransferToRow) && ( - - ethereumUtils.openAddressInBlockExplorer( - meta?.to?.address || toAddress || meta?.transferTo?.address || '', - ethereumUtils.getChainIdFromNetwork(currentNetwork) - ) - } - value={ - meta?.to?.name || - abbreviations.address(meta?.to?.address || toAddress, 4, 6) || - meta?.to?.address || - toAddress || - meta?.transferTo?.address || - '' - } - /> - )} - {showFunctionRow && } - {!!meta?.to?.sourceCodeStatus && } - {!!meta?.to?.created && } - {nonce && } - - - - - ); -}; - -const MessageCard = ({ - expandedCardBottomInset, - message, - method, -}: { - expandedCardBottomInset: number; - message: string; - method: RPCMethod; -}) => { - const { setClipboard } = useClipboard(); - const [didCopy, setDidCopy] = useState(false); - - let displayMessage = message; - if (isSignTypedData(method)) { - try { - const parsedMessage = JSON.parse(message); - const sanitizedMessage = sanitizeTypedData(parsedMessage); - displayMessage = sanitizedMessage; - // eslint-disable-next-line no-empty - } catch (e) { - logger.warn('[SignTransactionSheet]: Error while parsing message'); - } - - displayMessage = JSON.stringify(displayMessage, null, 4); - } - - const estimatedMessageHeight = useMemo(() => estimateMessageHeight(displayMessage), [displayMessage]); - - const cardHeight = useSharedValue( - estimatedMessageHeight > MAX_CARD_HEIGHT ? MAX_CARD_HEIGHT : estimatedMessageHeight + CARD_BORDER_WIDTH * 2 - ); - const contentHeight = useSharedValue(estimatedMessageHeight); - - const handleCopyPress = useCallback( - (message: string) => { - if (didCopy) return; - setClipboard(message); - setDidCopy(true); - const copyTimer = setTimeout(() => { - setDidCopy(false); - }, 2000); - return () => clearTimeout(copyTimer); - }, - [didCopy, setClipboard] - ); - - return ( - MAX_CARD_HEIGHT} - isExpanded - skipCollapsedState - > - - - - - - 􀙤 - - - - {i18n.t(i18n.l.walletconnect.simulation.message_card.title)} - - - - handleCopyPress(message)}> - - - - - - {i18n.t(i18n.l.walletconnect.simulation.message_card.copy)} - - - - - - - - - {displayMessage} - - - - ); -}; - -const SimulatedEventRow = ({ - amount, - asset, - eventType, - price, -}: { - amount: string | 'unlimited'; - asset: TransactionSimulationAsset | undefined; - eventType: EventType; - price?: number | undefined; -}) => { - const theme = useTheme(); - const { nativeCurrency } = useAccountSettings(); - const { data: externalAsset } = useExternalToken({ - address: asset?.assetCode || '', - chainId: ethereumUtils.getChainIdFromNetwork((asset?.network as Network) || Network.mainnet), - currency: nativeCurrency, - }); - - const eventInfo: EventInfo = infoForEventType[eventType]; - - const formattedAmount = useMemo(() => { - if (!asset) return; - - const nftFallbackSymbol = parseFloat(amount) > 1 ? 'NFTs' : 'NFT'; - const assetDisplayName = - asset?.type === TransactionAssetType.Nft ? asset?.name || asset?.symbol || nftFallbackSymbol : asset?.symbol || asset?.name; - const shortenedDisplayName = assetDisplayName.length > 12 ? `${assetDisplayName.slice(0, 12).trim()}…` : assetDisplayName; - - const displayAmount = - asset?.decimals === 0 - ? `${amount}${shortenedDisplayName ? ' ' + shortenedDisplayName : ''}` - : convertRawAmountToBalance(amount, { decimals: asset?.decimals || 18, symbol: shortenedDisplayName }, 3, true).display; - - const unlimitedApproval = `${i18n.t(i18n.l.walletconnect.simulation.simulation_card.event_row.unlimited)} ${asset?.symbol}`; - - return `${eventInfo.amountPrefix}${amount === 'UNLIMITED' ? unlimitedApproval : displayAmount}`; - }, [amount, asset, eventInfo?.amountPrefix]); - - const url = maybeSignUri(asset?.iconURL, { - fm: 'png', - w: 16 * PixelRatio.get(), - }); - - const showUSD = (eventType === 'send' || eventType === 'receive') && !!price; - - const formattedPrice = price && convertAmountToNativeDisplay(price, nativeCurrency); - - return ( - - - - - - - {eventInfo.label} - - {showUSD && ( - - {formattedPrice} - - )} - - - - - {asset?.type !== TransactionAssetType.Nft ? ( - - ) : ( - - )} - - - {formattedAmount} - - - - - ); -}; - -const DetailRow = ({ - currentNetwork, - detailType, - onPress, - value, -}: { - currentNetwork?: Network; - detailType: DetailType; - onPress?: () => void; - value: string; -}) => { - const detailInfo: DetailInfo = infoForDetailType[detailType]; - - return ( - - - - - - {detailInfo.label} - - - - {detailType === 'function' && } - {detailType === 'sourceCodeVerification' && ( - - )} - {detailType === 'chain' && currentNetwork && ( - - )} - {detailType !== 'function' && detailType !== 'sourceCodeVerification' && ( - - {value} - - )} - {(detailType === 'contract' || detailType === 'to') && ( - - - - - 􀂄 - - - - - )} - - - - ); -}; - -const EventIcon = ({ eventType }: { eventType: EventType }) => { - const eventInfo: EventInfo = infoForEventType[eventType]; - - const hideInnerFill = eventType === 'approve' || eventType === 'revoke'; - const isWarningIcon = - eventType === 'failed' || eventType === 'insufficientBalance' || eventType === 'MALICIOUS' || eventType === 'WARNING'; - - return ( - - {!hideInnerFill && ( - - )} - - {eventInfo.icon} - - - ); -}; - -const DetailIcon = ({ detailInfo }: { detailInfo: DetailInfo }) => { - return ( - - - {detailInfo.icon} - - - ); -}; - -const DetailBadge = ({ type, value }: { type: 'function' | 'unknown' | 'unverified' | 'verified'; value: string }) => { - const { colors, isDarkMode } = useTheme(); - const separatorTertiary = useForegroundColor('separatorTertiary'); - - const infoForBadgeType: { - [key: string]: { - backgroundColor: string; - borderColor: string; - label?: string; - text: TextColor; - textOpacity?: number; - }; - } = { - function: { - backgroundColor: 'transparent', - borderColor: isDarkMode ? separatorTertiary : colors.alpha(separatorTertiary, 0.025), - text: 'labelQuaternary', - }, - unknown: { - backgroundColor: 'transparent', - borderColor: isDarkMode ? separatorTertiary : colors.alpha(separatorTertiary, 0.025), - label: 'Unknown', - text: 'labelQuaternary', - }, - unverified: { - backgroundColor: isDarkMode ? colors.alpha(colors.red, 0.05) : globalColors.red10, - borderColor: colors.alpha(colors.red, 0.02), - label: 'Unverified', - text: 'red', - textOpacity: 0.76, - }, - verified: { - backgroundColor: isDarkMode ? colors.alpha(colors.green, 0.05) : globalColors.green10, - borderColor: colors.alpha(colors.green, 0.02), - label: 'Verified', - text: 'green', - textOpacity: 0.76, - }, - }; - - return ( - - - - {infoForBadgeType[type].label || value} - - - - ); -}; - -const VerifiedBadge = () => { - return ( - - - - - 􀇻 - - - - ); -}; - -const AnimatedCheckmark = ({ visible }: { visible: boolean }) => { - return ( - - {visible && ( - - - - - - 􀁣 - - - - - )} - - ); -}; - -const FadedScrollCard = ({ - cardHeight, - children, - contentHeight, - expandedCardBottomInset = 120, - expandedCardTopInset = 120, - initialScrollEnabled, - isExpanded, - onPressCollapsedCard, - skipCollapsedState, -}: { - cardHeight: SharedValue; - children: React.ReactNode; - contentHeight: SharedValue; - expandedCardBottomInset?: number; - expandedCardTopInset?: number; - initialScrollEnabled?: boolean; - isExpanded: boolean; - onPressCollapsedCard?: () => void; - skipCollapsedState?: boolean; -}) => { - const { height: deviceHeight, width: deviceWidth } = useDimensions(); - const { isDarkMode } = useTheme(); - - const cardRef = useAnimatedRef(); - - const [scrollEnabled, setScrollEnabled] = useState(initialScrollEnabled); - const [isFullyExpanded, setIsFullyExpanded] = useState(false); - - const yPosition = useSharedValue(0); - - const maxExpandedHeight = deviceHeight - (expandedCardBottomInset + expandedCardTopInset); - - const containerStyle = useAnimatedStyle(() => { - return { - height: - cardHeight.value > MAX_CARD_HEIGHT || !skipCollapsedState - ? interpolate( - cardHeight.value, - [MAX_CARD_HEIGHT, MAX_CARD_HEIGHT, maxExpandedHeight], - [cardHeight.value, MAX_CARD_HEIGHT, MAX_CARD_HEIGHT], - 'clamp' - ) - : undefined, - zIndex: interpolate(cardHeight.value, [0, MAX_CARD_HEIGHT, MAX_CARD_HEIGHT + 1], [1, 1, 2], 'clamp'), - }; - }); - - const backdropStyle = useAnimatedStyle(() => { - const canExpandFully = contentHeight.value + CARD_BORDER_WIDTH * 2 > MAX_CARD_HEIGHT; - return { - opacity: canExpandFully && isFullyExpanded ? withTiming(1, timingConfig) : withTiming(0, timingConfig), - }; - }); - - const cardStyle = useAnimatedStyle(() => { - const canExpandFully = contentHeight.value + CARD_BORDER_WIDTH * 2 > MAX_CARD_HEIGHT; - const expandedCardHeight = Math.min(contentHeight.value + CARD_BORDER_WIDTH * 2, maxExpandedHeight); - return { - borderColor: interpolateColor( - cardHeight.value, - [0, MAX_CARD_HEIGHT, expandedCardHeight], - isDarkMode ? ['#1F2023', '#1F2023', '#242527'] : ['#F5F7F8', '#F5F7F8', '#FBFCFD'] - ), - height: cardHeight.value > MAX_CARD_HEIGHT ? cardHeight.value : undefined, - position: canExpandFully && isFullyExpanded ? 'absolute' : 'relative', - transform: [ - { - translateY: interpolate( - cardHeight.value, - [0, MAX_CARD_HEIGHT, expandedCardHeight], - [ - 0, - 0, - -yPosition.value + - expandedCardTopInset + - (deviceHeight - (expandedCardBottomInset + expandedCardTopInset) - expandedCardHeight) - - (yPosition.value + expandedCardHeight >= deviceHeight - expandedCardBottomInset - ? 0 - : deviceHeight - expandedCardBottomInset - yPosition.value - expandedCardHeight), - ] - ), - }, - ], - }; - }); - - const centerVerticallyWhenCollapsedStyle = useAnimatedStyle(() => { - return { - transform: skipCollapsedState - ? undefined - : [ - { - translateY: interpolate( - cardHeight.value, - [ - 0, - COLLAPSED_CARD_HEIGHT, - contentHeight.value + CARD_BORDER_WIDTH * 2 > MAX_CARD_HEIGHT - ? MAX_CARD_HEIGHT - : contentHeight.value + CARD_BORDER_WIDTH * 2, - maxExpandedHeight, - ], - [-2, -2, 0, 0] - ), - }, - ], - }; - }); - - const shadowStyle = useAnimatedStyle(() => { - const canExpandFully = contentHeight.value + CARD_BORDER_WIDTH * 2 > MAX_CARD_HEIGHT; - return { - shadowOpacity: canExpandFully && isFullyExpanded ? withTiming(isDarkMode ? 0.9 : 0.16, timingConfig) : withTiming(0, timingConfig), - }; - }); - - const handleContentSizeChange = useCallback( - (width: number, height: number) => { - contentHeight.value = Math.round(height); - }, - [contentHeight] - ); - - const handleOnLayout = useCallback(() => { - runOnUI(() => { - if (cardHeight.value === MAX_CARD_HEIGHT) { - const measurement = measure(cardRef); - if (measurement === null) { - return; - } - if (yPosition.value !== measurement.pageY) { - yPosition.value = measurement.pageY; - } - } - })(); - }, [cardHeight, cardRef, yPosition]); - - useAnimatedReaction( - () => ({ contentHeight: contentHeight.value, isExpanded, isFullyExpanded }), - ({ contentHeight, isExpanded, isFullyExpanded }, previous) => { - if ( - isFullyExpanded !== previous?.isFullyExpanded || - isExpanded !== previous?.isExpanded || - contentHeight !== previous?.contentHeight - ) { - if (isFullyExpanded) { - const expandedCardHeight = - contentHeight + CARD_BORDER_WIDTH * 2 > maxExpandedHeight ? maxExpandedHeight : contentHeight + CARD_BORDER_WIDTH * 2; - if (contentHeight + CARD_BORDER_WIDTH * 2 > MAX_CARD_HEIGHT && cardHeight.value >= MAX_CARD_HEIGHT) { - cardHeight.value = withTiming(expandedCardHeight, timingConfig); - } else { - runOnJS(setIsFullyExpanded)(false); - } - } else if (isExpanded) { - cardHeight.value = withTiming( - contentHeight + CARD_BORDER_WIDTH * 2 > MAX_CARD_HEIGHT ? MAX_CARD_HEIGHT : contentHeight + CARD_BORDER_WIDTH * 2, - timingConfig - ); - } else { - cardHeight.value = withTiming(COLLAPSED_CARD_HEIGHT, timingConfig); - } - - const enableScroll = isExpanded && contentHeight + CARD_BORDER_WIDTH * 2 > (isFullyExpanded ? maxExpandedHeight : MAX_CARD_HEIGHT); - runOnJS(setScrollEnabled)(enableScroll); - } - } - ); - - return ( - - { - if (isFullyExpanded) { - setIsFullyExpanded(false); - } - }} - pointerEvents={isFullyExpanded ? 'auto' : 'none'} - style={[ - { - backgroundColor: 'rgba(0, 0, 0, 0.6)', - height: deviceHeight * 3, - left: -deviceWidth * 0.5, - position: 'absolute', - top: -deviceHeight, - width: deviceWidth * 2, - zIndex: -1, - }, - backdropStyle, - ]} - /> - - - - { - if (!isFullyExpanded) { - setIsFullyExpanded(true); - } else setIsFullyExpanded(false); - } - } - > - {children} - - - - - - - - ); -}; - -const FadeGradient = ({ side, style }: { side: 'top' | 'bottom'; style?: StyleProp>> }) => { - const { colors, isDarkMode } = useTheme(); - - const isTop = side === 'top'; - const solidColor = isDarkMode ? globalColors.white10 : '#FBFCFD'; - const transparentColor = colors.alpha(solidColor, 0); - - return ( - - - - ); -}; - -const IconContainer = ({ - children, - hitSlop, - opacity, - size = 20, -}: { - children: React.ReactNode; - hitSlop?: number; - opacity?: number; - size?: number; -}) => { - // Prevent wide icons from being clipped - const extraHorizontalSpace = 4; - - return ( - - - {children} - - - ); -}; - -type EventType = 'send' | 'receive' | 'approve' | 'revoke' | 'failed' | 'insufficientBalance' | 'MALICIOUS' | 'WARNING'; - -type EventInfo = { - amountPrefix: string; - icon: string; - iconColor: TextColor; - label: string; - textColor: TextColor; -}; - -const infoForEventType: { [key: string]: EventInfo } = { - send: { - amountPrefix: '- ', - icon: '􀁷', - iconColor: 'red', - label: i18n.t(i18n.l.walletconnect.simulation.simulation_card.event_row.types.send), - textColor: 'red', - }, - receive: { - amountPrefix: '+ ', - icon: '􀁹', - iconColor: 'green', - label: i18n.t(i18n.l.walletconnect.simulation.simulation_card.event_row.types.receive), - textColor: 'green', - }, - approve: { - amountPrefix: '', - icon: '􀎤', - iconColor: 'green', - label: i18n.t(i18n.l.walletconnect.simulation.simulation_card.event_row.types.approve), - textColor: 'label', - }, - revoke: { - amountPrefix: '', - icon: '􀎠', - iconColor: 'red', - label: i18n.t(i18n.l.walletconnect.simulation.simulation_card.event_row.types.revoke), - textColor: 'label', - }, - failed: { - amountPrefix: '', - icon: '􀇿', - iconColor: 'red', - label: i18n.t(i18n.l.walletconnect.simulation.simulation_card.titles.likely_to_fail), - textColor: 'red', - }, - insufficientBalance: { - amountPrefix: '', - icon: '􀇿', - iconColor: 'blue', - label: '', - textColor: 'blue', - }, - MALICIOUS: { - amountPrefix: '', - icon: '􀇿', - iconColor: 'red', - label: '', - textColor: 'red', - }, - WARNING: { - amountPrefix: '', - icon: '􀇿', - iconColor: 'orange', - label: '', - textColor: 'orange', - }, -}; - -type DetailType = 'chain' | 'contract' | 'to' | 'function' | 'sourceCodeVerification' | 'dateCreated' | 'nonce'; - -type DetailInfo = { - icon: string; - label: string; -}; - -const infoForDetailType: { [key: string]: DetailInfo } = { - chain: { - icon: '􀤆', - label: i18n.t(i18n.l.walletconnect.simulation.details_card.types.chain), - }, - contract: { - icon: '􀉆', - label: i18n.t(i18n.l.walletconnect.simulation.details_card.types.contract), - }, - to: { - icon: '􀉩', - label: i18n.t(i18n.l.walletconnect.simulation.details_card.types.to), - }, - function: { - icon: '􀡅', - label: i18n.t(i18n.l.walletconnect.simulation.details_card.types.function), - }, - sourceCodeVerification: { - icon: '􀕹', - label: i18n.t(i18n.l.walletconnect.simulation.details_card.types.source_code), - }, - dateCreated: { - icon: '􀉉', - label: i18n.t(i18n.l.walletconnect.simulation.details_card.types.contract_created), - }, - nonce: { - icon: '􀆃', - label: i18n.t(i18n.l.walletconnect.simulation.details_card.types.nonce), - }, -}; - -const CHARACTERS_PER_LINE = 40; -const LINE_HEIGHT = 11; -const LINE_GAP = 9; - -const estimateMessageHeight = (message: string) => { - const estimatedLines = Math.ceil(message.length / CHARACTERS_PER_LINE); - const messageHeight = estimatedLines * LINE_HEIGHT + (estimatedLines - 1) * LINE_GAP + CARD_ROW_HEIGHT + 24 * 3 - CARD_BORDER_WIDTH * 2; - - return messageHeight; -}; - -const formatDate = (dateString: string) => { - const date = new Date(dateString); - const now = new Date(); - const diffTime = Math.abs(now.getTime() - date.getTime()); - const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); - const diffWeeks = Math.floor(diffDays / 7); - const diffMonths = Math.floor(diffDays / 30.44); - - if (diffDays === 0) { - return i18n.t(i18n.l.walletconnect.simulation.formatted_dates.today); - } else if (diffDays === 1) { - return `${diffDays} ${i18n.t(i18n.l.walletconnect.simulation.formatted_dates.day_ago)}`; - } else if (diffDays < 7) { - return `${diffDays} ${i18n.t(i18n.l.walletconnect.simulation.formatted_dates.days_ago)}`; - } else if (diffWeeks === 1) { - return `${diffWeeks} ${i18n.t(i18n.l.walletconnect.simulation.formatted_dates.week_ago)}`; - } else if (diffDays < 30.44) { - return `${diffWeeks} ${i18n.t(i18n.l.walletconnect.simulation.formatted_dates.weeks_ago)}`; - } else if (diffMonths === 1) { - return `${diffMonths} ${i18n.t(i18n.l.walletconnect.simulation.formatted_dates.month_ago)}`; - } else if (diffDays < 365.25) { - return `${diffMonths} ${i18n.t(i18n.l.walletconnect.simulation.formatted_dates.months_ago)}`; - } else { - return date.toLocaleString('default', { month: 'short', year: 'numeric' }); - } -}; diff --git a/src/screens/SpeedUpAndCancelSheet.js b/src/screens/SpeedUpAndCancelSheet.js index 521fee3648f..0226d4e9323 100644 --- a/src/screens/SpeedUpAndCancelSheet.js +++ b/src/screens/SpeedUpAndCancelSheet.js @@ -1,5 +1,4 @@ import { useRoute } from '@react-navigation/native'; -import { captureException } from '@sentry/react-native'; import { BigNumber } from 'bignumber.js'; import lang from 'i18n-js'; import { isEmpty } from 'lodash'; @@ -17,7 +16,7 @@ import { Emoji, Text } from '../components/text'; import { WrappedAlert as Alert } from '@/helpers/alert'; import { removeRegistrationByName, saveCommitRegistrationParameters } from '@/redux/ensRegistration'; import { GasFeeTypes } from '@/entities'; -import { getFlashbotsProvider, getProviderForNetwork, isL2Chain, toHex } from '@/handlers/web3'; +import { getFlashbotsProvider, getProvider, isL2Chain, toHex } from '@/handlers/web3'; import { greaterThan } from '@/helpers/utilities'; import { useAccountSettings, useDimensions, useGas, useWallets } from '@/hooks'; import { sendTransaction } from '@/model/wallet'; @@ -27,9 +26,9 @@ import { updateGasFeeForSpeed } from '@/redux/gas'; import { ethUnits } from '@/references'; import styled from '@/styled-thing'; import { position } from '@/styles'; -import { ethereumUtils, gasUtils, safeAreaInsetValues } from '@/utils'; +import { gasUtils, safeAreaInsetValues } from '@/utils'; import { logger, RainbowError } from '@/logger'; -import { getNetworkObj } from '@/networks'; +import { getNetworkObject } from '@/networks'; import * as i18n from '@/languages'; import { updateTransaction } from '@/state/pendingTransactions'; @@ -101,7 +100,7 @@ const calcGasParamRetryValue = prevWeiValue => { export default function SpeedUpAndCancelSheet() { const { navigate, goBack } = useNavigation(); - const { accountAddress, network } = useAccountSettings(); + const { accountAddress, chainId } = useAccountSettings(); const { isHardwareWallet } = useWallets(); const dispatch = useDispatch(); const { height: deviceHeight } = useDimensions(); @@ -118,7 +117,7 @@ export default function SpeedUpAndCancelSheet() { const [minMaxPriorityFeePerGas, setMinMaxPriorityFeePerGas] = useState(calcGasParamRetryValue(tx.maxPriorityFeePerGas)); const [minMaxFeePerGas, setMinMaxFeePerGas] = useState(calcGasParamRetryValue(tx.maxFeePerGas)); const fetchedTx = useRef(false); - const [currentNetwork, setCurrentNetwork] = useState(null); + const [currentChainId, setCurrentChainId] = useState(null); const [currentProvider, setCurrentProvider] = useState(null); const [data, setData] = useState(null); const [gasLimit, setGasLimit] = useState(null); @@ -172,7 +171,11 @@ export default function SpeedUpAndCancelSheet() { updatedTx.hash = res.result?.hash; updatedTx.status = 'pending'; updatedTx.type = 'cancel'; - updateTransaction({ address: accountAddress, transaction: updatedTx, network: currentNetwork }); + updateTransaction({ + address: accountAddress, + transaction: updatedTx, + chainId: currentChainId, + }); } catch (e) { logger.error(new RainbowError(`[SpeedUpAndCancelSheet]: error submitting cancel tx: ${e}`)); } finally { @@ -185,7 +188,7 @@ export default function SpeedUpAndCancelSheet() { }, [ accountAddress, cancelCommitTransactionHash, - currentNetwork, + currentChainId, currentProvider, getNewTransactionGasParams, goBack, @@ -242,7 +245,11 @@ export default function SpeedUpAndCancelSheet() { updatedTx.status = 'pending'; updatedTx.type = 'speed_up'; - updateTransaction({ address: accountAddress, transaction: updatedTx, network: currentNetwork }); + updateTransaction({ + address: accountAddress, + transaction: updatedTx, + chainId: currentChainId, + }); } catch (e) { logger.error(new RainbowError(`[SpeedUpAndCancelSheet]: error submitting speed up tx: ${e}`)); } finally { @@ -254,7 +261,7 @@ export default function SpeedUpAndCancelSheet() { } }, [ accountAddress, - currentNetwork, + currentChainId, currentProvider, data, gasLimit, @@ -278,21 +285,21 @@ export default function SpeedUpAndCancelSheet() { // Set the network useEffect(() => { - setCurrentNetwork(tx?.network || network); - }, [network, tx.network]); + setCurrentChainId(tx?.chainId || chainId); + }, [chainId, tx.chainId]); // Set the provider useEffect(() => { - if (currentNetwork) { - startPollingGasFees(currentNetwork, tx.flashbots); + if (currentChainId) { + startPollingGasFees(currentChainId, tx.flashbots); const updateProvider = async () => { let provider; - if (getNetworkObj(tx?.network).features.flashbots && tx.flashbots) { - logger.debug(`[SpeedUpAndCancelSheet]: using flashbots provider for network ${currentNetwork}`); + if (getNetworkObject({ chainId: tx?.chainId })?.features?.flashbots && tx.flashbots) { + logger.debug(`[SpeedUpAndCancelSheet]: using flashbots provider for chainId ${tx?.chainId}`); provider = await getFlashbotsProvider(); } else { - logger.debug(`[SpeedUpAndCancelSheet]: using provider for network ${currentNetwork}`); - provider = getProviderForNetwork(currentNetwork); + logger.debug(`[SpeedUpAndCancelSheet]: using provider for network ${tx?.chainId}`); + provider = getProvider({ chainId: currentChainId }); } setCurrentProvider(provider); }; @@ -303,7 +310,7 @@ export default function SpeedUpAndCancelSheet() { stopPollingGasFees(); }; } - }, [currentNetwork, startPollingGasFees, stopPollingGasFees, tx.flashbots, tx?.network]); + }, [currentChainId, startPollingGasFees, stopPollingGasFees, tx?.chainId, tx.flashbots]); // Update gas limit useEffect(() => { @@ -313,11 +320,11 @@ export default function SpeedUpAndCancelSheet() { updateGasFeeOption(gasUtils.URGENT); speedUrgentSelected.current = true; } - }, [currentNetwork, gasLimit, gasFeeParamsBySpeed, updateGasFeeOption, updateTxFee]); + }, [gasLimit, gasFeeParamsBySpeed, updateGasFeeOption, updateTxFee]); useEffect(() => { const init = async () => { - if (currentNetwork && currentProvider && !fetchedTx.current) { + if (currentChainId && currentProvider && !fetchedTx.current) { try { fetchedTx.current = true; const hexGasLimit = toHex(tx?.gasLimit?.toString() || '0x'); @@ -361,7 +368,7 @@ export default function SpeedUpAndCancelSheet() { }; init(); - }, [currentNetwork, currentProvider, goBack, isL2, network, tx, tx.gasLimit, tx.hash, type, updateGasFeeOption]); + }, [currentChainId, currentProvider, goBack, isL2, tx, tx?.gasLimit, tx.hash, type, updateGasFeeOption]); useEffect(() => { if (!isEmpty(gasFeeParamsBySpeed) && !calculatingGasLimit.current) { @@ -475,8 +482,7 @@ export default function SpeedUpAndCancelSheet() { ({ color: colors.alpha(colors.blueGreyDark, 0.3), @@ -69,8 +67,8 @@ const NetworkPill = ({ chainIds }) => { const availableNetworkChainIds = useMemo(() => chainIds.sort(chainId => (chainId === ChainId.mainnet ? -1 : 1)), [chainIds]); const networkMenuItems = useMemo(() => { - RainbowNetworks.filter(({ features, id }) => features.walletconnect && chainIds.includes(id)).map(network => ({ - actionKey: network.value, + RainbowNetworkObjects.filter(({ features, id }) => features.walletconnect && chainIds.includes(id)).map(network => ({ + actionKey: network.id, actionTitle: network.name, icon: { iconType: 'ASSET', @@ -128,7 +126,7 @@ const NetworkPill = ({ chainIds }) => { ) : ( - {availableNetworkChainIds[0] !== Network.mainnet ? ( + {availableNetworkChainIds[0] !== ChainId.mainnet ? ( ) : ( @@ -136,7 +134,7 @@ const NetworkPill = ({ chainIds }) => { - {getNetworkObj(availableNetworkChainIds[0]).name} + {chainIdToNameMapping[availableNetworkChainIds[0]]} @@ -151,7 +149,7 @@ export default function WalletConnectApprovalSheet() { const { colors, isDarkMode } = useTheme(); const { goBack } = useNavigation(); const { params } = useRoute(); - const { network, accountAddress } = useAccountSettings(); + const { chainId: settingsChainId, accountAddress } = useAccountSettings(); const { navigate } = useNavigation(); const { selectedWallet, walletNames, wallets } = useWallets(); const handled = useRef(false); @@ -182,8 +180,8 @@ export default function WalletConnectApprovalSheet() { const failureExplainSheetVariant = params?.failureExplainSheetVariant; const chainIds = meta?.chainIds; // WC v2 supports multi-chain const chainId = meta?.proposedChainId || chainIds?.[0] || 1; // WC v1 only supports 1 - const currentNetwork = params?.currentNetwork; - const [approvalNetwork, setApprovalNetwork] = useState(currentNetwork || network); + const currentChainId = params?.currentChainId; + const [approvalChainId, setApprovalChainId] = useState(currentChainId || settingsChainId); const isWalletConnectV2 = meta.isWalletConnectV2; const { dappName, dappUrl, dappScheme, imageUrl, peerId } = meta; @@ -225,18 +223,18 @@ export default function WalletConnectApprovalSheet() { * v2. */ const approvalNetworkInfo = useMemo(() => { - const networkObj = getNetworkObj(approvalNetwork); + const networkObj = getNetworkObject({ chainId: approvalChainId }); return { chainId: networkObj.id, color: isDarkMode ? networkObj.colors.dark : networkObj.colors.light, name: networkObj.name, value: networkObj.value, }; - }, [approvalNetwork, isDarkMode]); + }, [approvalChainId, isDarkMode]); const handleOnPressNetworksMenuItem = useCallback( - ({ nativeEvent }) => setApprovalNetwork(nativeEvent.actionKey?.replace(NETWORK_MENU_ACTION_KEY_FILTER, '')), - [setApprovalNetwork] + ({ nativeEvent }) => setApprovalChainId(nativeEvent.actionKey?.replace(NETWORK_MENU_ACTION_KEY_FILTER, '')), + [setApprovalChainId] ); const handleSuccess = useCallback( @@ -253,8 +251,7 @@ export default function WalletConnectApprovalSheet() { useEffect(() => { if (chainId && type === WalletConnectApprovalSheetType.connect) { - const network = ethereumUtils.getNetworkFromChainId(Number(chainId)); - setApprovalNetwork(network); + setApprovalChainId(chainId); } }, [chainId, type]); @@ -284,7 +281,7 @@ export default function WalletConnectApprovalSheet() { }, [handleSuccess, goBack]); const onPressAndroid = useCallback(() => { - androidShowNetworksActionSheet(({ network }) => setApprovalNetwork(network)); + androidShowNetworksActionSheet(({ chainId }) => setApprovalChainId(chainId)); }, []); const handlePressChangeWallet = useCallback(() => { @@ -358,19 +355,11 @@ export default function WalletConnectApprovalSheet() { }} > - + {`${ - type === WalletConnectApprovalSheetType.connect - ? approvalNetworkInfo.name - : ethereumUtils.getNetworkNameFromChainId(Number(chainId)) + type === WalletConnectApprovalSheetType.connect ? approvalNetworkInfo.name : chainIdToNameMapping[chainId] } ${type === WalletConnectApprovalSheetType.connect && menuItems.length > 1 ? '􀁰' : ''}`} @@ -379,8 +368,8 @@ export default function WalletConnectApprovalSheet() { } }, [ NetworkSwitcherParent, + approvalNetworkInfo.chainId, approvalNetworkInfo.name, - approvalNetworkInfo.value, chainId, chainIds, handleOnPressNetworksMenuItem, @@ -411,17 +400,19 @@ export default function WalletConnectApprovalSheet() { {type === WalletConnectApprovalSheetType.connect ? lang.t(lang.l.walletconnect.wants_to_connect) : lang.t(lang.l.walletconnect.wants_to_connect_to_network, { - network: ethereumUtils.getNetworkNameFromChainId(Number(chainId)), + network: chainIdToNameMapping[chainId], })} - - {isScam && '􁅏 '} - {isVerified && '􀇻 '} - {formattedDappUrl} - + {formattedDappUrl && ( + + {isScam && '􁅏 '} + {isVerified && '􀇻 '} + {formattedDappUrl} + + )} diff --git a/src/screens/WalletScreen/index.tsx b/src/screens/WalletScreen/index.tsx index 02ea352ec5c..e8b297d502e 100644 --- a/src/screens/WalletScreen/index.tsx +++ b/src/screens/WalletScreen/index.tsx @@ -1,22 +1,17 @@ -import { InteractionManager, View } from 'react-native'; -import React, { useCallback, useEffect, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { View } from 'react-native'; +import React, { useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; import { AssetList } from '../../components/asset-list'; import { Page } from '../../components/layout'; -import { Network } from '@/helpers'; import { useRemoveFirst } from '@/navigation/useRemoveFirst'; -import { settingsUpdateNetwork } from '@/redux/settings'; import { navbarHeight } from '@/components/navbar/Navbar'; import { Box } from '@/design-system'; import { useAccountAccentColor, useAccountSettings, - useInitializeAccountData, useInitializeWallet, - useLoadAccountData, useLoadAccountLateData, useLoadGlobalLateData, - useResetAccountState, useWalletSectionsData, } from '@/hooks'; import Routes from '@rainbow-me/routes'; @@ -33,6 +28,8 @@ import { IS_ANDROID } from '@/env'; import { RemoteCardsSync } from '@/state/sync/RemoteCardsSync'; import { RemotePromoSheetSync } from '@/state/sync/RemotePromoSheetSync'; import { UserAssetsSync } from '@/state/sync/UserAssetsSync'; +import { MobileWalletProtocolListener } from '@/components/MobileWalletProtocolListener'; +import { runWalletBackupStatusChecks } from '@/handlers/walletReadyEvents'; const WalletPage = styled(Page)({ ...position.sizeAsObject('100%'), @@ -51,28 +48,8 @@ const WalletScreen: React.FC = ({ navigation, route }) => { const loadAccountLateData = useLoadAccountLateData(); const loadGlobalLateData = useLoadGlobalLateData(); - const dispatch = useDispatch(); - const resetAccountState = useResetAccountState(); - const loadAccountData = useLoadAccountData(); - const initializeAccountData = useInitializeAccountData(); const insets = useSafeAreaInsets(); - const revertToMainnet = useCallback(async () => { - await resetAccountState(); - await dispatch(settingsUpdateNetwork(Network.mainnet)); - InteractionManager.runAfterInteractions(async () => { - await loadAccountData(); - initializeAccountData(); - }); - }, [dispatch, initializeAccountData, loadAccountData, resetAccountState]); - - useEffect(() => { - const supportedNetworks = [Network.mainnet]; - if (!supportedNetworks.includes(currentNetwork)) { - revertToMainnet(); - } - }, [currentNetwork, revertToMainnet]); - const walletReady = useSelector(({ appState: { walletReady } }: AppState) => walletReady); const { isWalletEthZero, isLoadingUserAssets, isLoadingBalance, briefSectionsData: walletBriefSectionsData } = useWalletSectionsData(); @@ -106,6 +83,7 @@ const WalletScreen: React.FC = ({ navigation, route }) => { if (walletReady) { loadAccountLateData(); loadGlobalLateData(); + runWalletBackupStatusChecks(); } }, [loadAccountLateData, loadGlobalLateData, walletReady]); @@ -147,6 +125,9 @@ const WalletScreen: React.FC = ({ navigation, route }) => { + + {/* NOTE: This component listens for Mobile Wallet Protocol requests and handles them */} + ); diff --git a/src/screens/discover/components/DiscoverHome.tsx b/src/screens/discover/components/DiscoverHome.tsx index 67a6e8ce011..866c7688096 100644 --- a/src/screens/discover/components/DiscoverHome.tsx +++ b/src/screens/discover/components/DiscoverHome.tsx @@ -7,7 +7,7 @@ import useExperimentalFlag, { NFT_OFFERS, FEATURED_RESULTS, } from '@rainbow-me/config/experimentalHooks'; -import { isTestnetNetwork } from '@/handlers/web3'; +import { isTestnetChain } from '@/handlers/web3'; import { Inline, Inset, Stack, Box } from '@/design-system'; import { useAccountSettings, useWallets } from '@/hooks'; import { ENSCreateProfileCard } from '@/components/cards/ENSCreateProfileCard'; @@ -33,7 +33,7 @@ export const HORIZONTAL_PADDING = 20; export default function DiscoverHome() { const { profiles_enabled, mints_enabled, op_rewards_enabled, featured_results } = useRemoteConfig(); - const { network } = useAccountSettings(); + const { chainId } = useAccountSettings(); const profilesEnabledLocalFlag = useExperimentalFlag(PROFILES); const profilesEnabledRemoteFlag = profiles_enabled; const hardwareWalletsEnabled = useExperimentalFlag(HARDWARE_WALLETS); @@ -42,7 +42,7 @@ export default function DiscoverHome() { const mintsEnabled = (useExperimentalFlag(MINTS) || mints_enabled) && !IS_TEST; const opRewardsLocalFlag = useExperimentalFlag(OP_REWARDS); const opRewardsRemoteFlag = op_rewards_enabled; - const testNetwork = isTestnetNetwork(network); + const testNetwork = isTestnetChain({ chainId }); const { navigate } = useNavigation(); const isProfilesEnabled = profilesEnabledLocalFlag && profilesEnabledRemoteFlag; diff --git a/src/screens/discover/components/DiscoverSearch.js b/src/screens/discover/components/DiscoverSearch.js index 74233112e6b..52741d9fc4e 100644 --- a/src/screens/discover/components/DiscoverSearch.js +++ b/src/screens/discover/components/DiscoverSearch.js @@ -19,11 +19,10 @@ import Routes from '@/navigation/routesNames'; import styled from '@/styled-thing'; import { useTheme } from '@/theme'; import { ethereumUtils } from '@/utils'; -import { Network } from '@/helpers'; import { getPoapAndOpenSheetWithQRHash, getPoapAndOpenSheetWithSecretWord } from '@/utils/poaps'; import { navigateToMintCollection } from '@/resources/reservoir/mints'; import { TAB_BAR_HEIGHT } from '@/navigation/SwipeNavigator'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId, Network } from '@/networks/types'; export const SearchContainer = styled(Row)({ height: '100%', @@ -134,7 +133,7 @@ export default function DiscoverSearch() { network === Network.optimism; } const contractAddress = query.split('/')[1]; - navigateToMintCollection(contractAddress, network); + navigateToMintCollection(contractAddress, ethereumUtils.getChainIdFromNetwork(network)); } }; checkAndHandleMint(searchQuery); diff --git a/src/screens/mints/MintSheet.tsx b/src/screens/mints/MintSheet.tsx index 596e7c6f150..399b8ca3e56 100644 --- a/src/screens/mints/MintSheet.tsx +++ b/src/screens/mints/MintSheet.tsx @@ -56,7 +56,7 @@ import { getUniqueId } from '@/utils/ethereumUtils'; import { getNextNonce } from '@/state/nonces'; import { metadataPOSTClient } from '@/graphql'; import { Transaction } from '@/graphql/__generated__/metadataPOST'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; const NFT_IMAGE_HEIGHT = 250; // inset * 2 -> 28 *2 @@ -200,7 +200,7 @@ const MintSheet = () => { // check address balance useEffect(() => { const checkInsufficientEth = async () => { - const nativeBalance = (await ethereumUtils.getNativeAssetForNetwork(chainId, accountAddress))?.balance?.amount ?? 0; + const nativeBalance = (await ethereumUtils.getNativeAssetForNetwork({ chainId, address: accountAddress }))?.balance?.amount ?? 0; const totalMintPrice = multiply(price.amount, quantity); if (greaterThanOrEqualTo(totalMintPrice, nativeBalance)) { @@ -237,8 +237,7 @@ const MintSheet = () => { // start poll gas price useEffect(() => { - const network = ethereumUtils.getNetworkFromChainId(chainId); - startPollingGasFees(network); + startPollingGasFees(chainId); return () => { stopPollingGasFees(); @@ -365,9 +364,8 @@ const MintSheet = () => { }); const feeAddress = getRainbowFeeAddress(chainId); - const nonce = await getNextNonce({ address: accountAddress, network: ethereumUtils.getNetworkFromChainId(chainId) }); + const nonce = await getNextNonce({ address: accountAddress, chainId }); try { - const currentNetwork = ethereumUtils.getNetworkFromChainId(chainId); await getClient()?.actions.mintToken({ items: [ { @@ -388,10 +386,11 @@ const MintSheet = () => { step.items?.forEach(item => { if (item.txHashes?.[0]?.txHash && txRef.current !== item.txHashes[0].txHash && item.status === 'incomplete') { const asset = { + chainId, type: 'nft', icon_url: imageUrl, address: mintCollection.id || '', - network: currentNetwork, + network: ethereumUtils.getNetworkFromChainId(chainId), name: mintCollection.name || '', decimals: 18, symbol: 'NFT', @@ -401,7 +400,7 @@ const MintSheet = () => { const paymentAsset = { type: 'nft', address: ETH_ADDRESS, - network: currentNetwork, + network: ethereumUtils.getNetworkFromChainId(chainId), name: mintCollection.publicMintInfo?.price?.currency?.name || 'Ethereum', decimals: mintCollection.publicMintInfo?.price?.currency?.decimals || 18, symbol: ETH_SYMBOL, @@ -414,7 +413,7 @@ const MintSheet = () => { to: item.data?.to, from: item.data?.from, hash: item.txHashes[0].txHash, - network: currentNetwork, + network: ethereumUtils.getNetworkFromChainId(chainId), nonce, changes: [ { @@ -437,7 +436,7 @@ const MintSheet = () => { addNewTransaction({ transaction: tx, address: accountAddress, - network: currentNetwork, + chainId, }); analyticsV2.track(event.mintsMintedNFT, { collectionName: mintCollection.name || '', @@ -683,7 +682,9 @@ const MintSheet = () => { symbol="􀉆" label={i18n.t(i18n.l.minting.contract)} value={ - ethereumUtils.openAddressInBlockExplorer(mintCollection.id!, chainId)}> + ethereumUtils.openAddressInBlockExplorer({ address: mintCollection.id!, chainId })} + > {contractAddressDisplay} diff --git a/src/screens/points/claim-flow/ClaimRewardsPanel.tsx b/src/screens/points/claim-flow/ClaimRewardsPanel.tsx index a1e710bfe53..7fa9780b228 100644 --- a/src/screens/points/claim-flow/ClaimRewardsPanel.tsx +++ b/src/screens/points/claim-flow/ClaimRewardsPanel.tsx @@ -5,7 +5,7 @@ import { Bleed, Box, Text, TextShadow, globalColors, useBackgroundColor, useColo import * as i18n from '@/languages'; import { ListHeader, ListPanel, Panel, TapToDismiss, controlPanelStyles } from '@/components/SmoothPager/ListPanel'; import { ChainImage } from '@/components/coin-icon/ChainImage'; -import { ChainId, ChainNameDisplay } from '@/__swaps__/types/chains'; +import { ChainId, ChainNameDisplay } from '@/networks/types'; import ethereumUtils, { useNativeAsset } from '@/utils/ethereumUtils'; import { useAccountAccentColor, useAccountProfile, useAccountSettings } from '@/hooks'; import { safeAreaInsetValues } from '@/utils'; @@ -17,7 +17,6 @@ import { PointsErrorType } from '@/graphql/__generated__/metadata'; import { useMutation } from '@tanstack/react-query'; import { invalidatePointsQuery, usePoints } from '@/resources/points'; import { convertAmountAndPriceToNativeDisplay, convertRawAmountToBalance } from '@/helpers/utilities'; -import { Network } from '@/helpers'; import { ButtonPressAnimation } from '@/components/animations'; import { DEVICE_WIDTH } from '@/utils/deviceUtils'; import { TIMING_CONFIGS } from '@/components/animations/animationConfigs'; @@ -29,7 +28,7 @@ import { walletExecuteRap } from '@/raps/execute'; import { ParsedAsset } from '@/__swaps__/types/assets'; import { chainNameFromChainId } from '@/__swaps__/utils/chains'; import { loadWallet } from '@/model/wallet'; -import { getProviderForNetwork } from '@/handlers/web3'; +import { getProvider } from '@/handlers/web3'; import { LegacyTransactionGasParamAmounts, TransactionGasParamAmounts } from '@/entities'; import { getGasSettingsBySpeed } from '@/__swaps__/screens/Swap/hooks/useSelectedGas'; import { useMeteorologySuggestions } from '@/__swaps__/utils/meteorology'; @@ -211,7 +210,7 @@ const ClaimingRewards = ({ }>({ mutationFn: async () => { // Fetch the native asset from the origin chain - const opEth_ = await ethereumUtils.getNativeAssetForNetwork(ChainId.optimism); + const opEth_ = await ethereumUtils.getNativeAssetForNetwork({ chainId: ChainId.optimism }); const opEth = { ...opEth_, chainName: chainNameFromChainId(ChainId.optimism), @@ -220,9 +219,9 @@ const ClaimingRewards = ({ // Fetch the native asset from the destination chain let destinationEth_; if (chainId === ChainId.base) { - destinationEth_ = await ethereumUtils.getNativeAssetForNetwork(ChainId.base); + destinationEth_ = await ethereumUtils.getNativeAssetForNetwork({ chainId: ChainId.base }); } else if (chainId === ChainId.zora) { - destinationEth_ = await ethereumUtils.getNativeAssetForNetwork(ChainId.zora); + destinationEth_ = await ethereumUtils.getNativeAssetForNetwork({ chainId: ChainId.zora }); } else { destinationEth_ = opEth; } @@ -261,7 +260,7 @@ const ClaimingRewards = ({ gasParams, } satisfies RapSwapActionParameters<'claimBridge'>; - const provider = getProviderForNetwork(Network.optimism); + const provider = getProvider({ chainId: ChainId.optimism }); const wallet = await loadWallet({ address, showErrorIfNotLoaded: false, diff --git a/src/screens/points/components/LeaderboardRow.tsx b/src/screens/points/components/LeaderboardRow.tsx index 357630f1aa3..915e6c22a8f 100644 --- a/src/screens/points/components/LeaderboardRow.tsx +++ b/src/screens/points/components/LeaderboardRow.tsx @@ -9,7 +9,6 @@ import { RAINBOW_PROFILES_BASE_URL } from '@/references'; import Routes from '@/navigation/routesNames'; import { ethereumUtils, isENSNFTRecord } from '@/utils'; import { address as formatAddress } from '@/utils/abbreviations'; -import { Network } from '@/networks/types'; import { ContactAvatar, showDeleteContactActionSheet } from '@/components/contacts'; import { Bleed, Box, Inline, Stack, Text } from '@/design-system'; import MaskedView from '@react-native-masked-view/masked-view'; @@ -20,7 +19,7 @@ import { useTheme } from '@/theme'; import LinearGradient from 'react-native-linear-gradient'; import { ButtonPressAnimation } from '@/components/animations'; import { noop } from 'lodash'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; const ACTIONS = { ADD_CONTACT: 'add-contact', @@ -128,7 +127,7 @@ export const LeaderboardRow = memo(function LeaderboardRow({ setClipboard(address); } if (address && actionKey === ACTIONS.ETHERSCAN) { - ethereumUtils.openAddressInBlockExplorer(address, ChainId.mainnet); + ethereumUtils.openAddressInBlockExplorer({ address: address, chainId: ChainId.mainnet }); } if (actionKey === ACTIONS.ADD_CONTACT) { navigate(Routes.MODAL_SCREEN, { diff --git a/src/screens/points/content/PointsContent.tsx b/src/screens/points/content/PointsContent.tsx index e6e6012c8ed..c90c006f7af 100644 --- a/src/screens/points/content/PointsContent.tsx +++ b/src/screens/points/content/PointsContent.tsx @@ -62,7 +62,7 @@ import { format, intervalToDuration, isToday } from 'date-fns'; import { useRemoteConfig } from '@/model/remoteConfig'; import { ETH_REWARDS, useExperimentalFlag } from '@/config'; import { RewardsActionButton } from '../components/RewardsActionButton'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; const InfoCards = ({ points }: { points: GetPointsDataForWalletQuery | undefined }) => { const labelSecondary = useForegroundColor('labelSecondary'); diff --git a/src/screens/points/contexts/PointsProfileContext.tsx b/src/screens/points/contexts/PointsProfileContext.tsx index 93d8bd841d0..bed940f82a9 100644 --- a/src/screens/points/contexts/PointsProfileContext.tsx +++ b/src/screens/points/contexts/PointsProfileContext.tsx @@ -13,10 +13,10 @@ import { loadWallet, signPersonalMessage } from '@/model/wallet'; import { RainbowError, logger } from '@/logger'; import { queryClient } from '@/react-query'; import { useNavigation } from '@/navigation'; -import { getProviderForNetwork } from '@/handlers/web3'; -import { Network } from '@/networks/types'; +import { getProvider } from '@/handlers/web3'; import { analyticsV2 } from '@/analytics'; import { delay } from '@/utils/delay'; +import { ChainId } from '@/networks/types'; type PointsProfileContext = { step: RainbowPointsFlowSteps; @@ -134,7 +134,7 @@ export const PointsProfileProvider = ({ children }: { children: React.ReactNode Alert.alert(i18n.t(i18n.l.points.console.generic_alert)); throw new RainbowError('Points: Error getting onboard challenge'); } - const provider = getProviderForNetwork(Network.mainnet); + const provider = getProvider({ chainId: ChainId.mainnet }); const wallet = await loadWallet({ address: accountAddress, provider }); if (!wallet) { Alert.alert(i18n.t(i18n.l.points.console.generic_alert)); diff --git a/src/screens/transaction-details/components/TransactionDetailsHashAndActionsSection.tsx b/src/screens/transaction-details/components/TransactionDetailsHashAndActionsSection.tsx index 223fa5d7d06..9040b9a8e1b 100644 --- a/src/screens/transaction-details/components/TransactionDetailsHashAndActionsSection.tsx +++ b/src/screens/transaction-details/components/TransactionDetailsHashAndActionsSection.tsx @@ -101,7 +101,7 @@ export const TransactionDetailsHashAndActionsSection: React.FC = ({ trans weight="heavy" onPress={onViewOnBlockExplorerPress} label={i18n.t(i18n.l.wallet.action.view_on, { - blockExplorerName: transaction.explorerLabel ?? startCase(ethereumUtils.getBlockExplorer(chainId)), + blockExplorerName: transaction.explorerLabel ?? startCase(ethereumUtils.getBlockExplorer({ chainId })), })} lightShadows /> diff --git a/src/screens/transaction-details/components/TransactionDetailsValueAndFeeSection.tsx b/src/screens/transaction-details/components/TransactionDetailsValueAndFeeSection.tsx index d0790ebe0e6..5672b52ae02 100644 --- a/src/screens/transaction-details/components/TransactionDetailsValueAndFeeSection.tsx +++ b/src/screens/transaction-details/components/TransactionDetailsValueAndFeeSection.tsx @@ -6,8 +6,6 @@ import { Box, Stack, globalColors } from '@/design-system'; import { TransactionDetailsDivider } from '@/screens/transaction-details/components/TransactionDetailsDivider'; import * as i18n from '@/languages'; -import { Network } from '@/networks/types'; - import RainbowCoinIcon from '@/components/coin-icon/RainbowCoinIcon'; import { convertAmountAndPriceToNativeDisplay, convertRawAmountToBalance } from '@/helpers/utilities'; import { useAccountSettings } from '@/hooks'; @@ -17,7 +15,7 @@ import ImgixImage from '@/components/images/ImgixImage'; import { View } from 'react-native'; import ChainBadge from '@/components/coin-icon/ChainBadge'; import { checkForPendingSwap } from '../helpers/checkForPendingSwap'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; type Props = { transaction: RainbowTransaction; diff --git a/src/screens/transaction-details/components/TransactionMasthead.tsx b/src/screens/transaction-details/components/TransactionMasthead.tsx index 7461caddc5d..92c16db5100 100644 --- a/src/screens/transaction-details/components/TransactionMasthead.tsx +++ b/src/screens/transaction-details/components/TransactionMasthead.tsx @@ -34,7 +34,7 @@ import ImageAvatar from '@/components/contacts/ImageAvatar'; import RainbowCoinIcon from '@/components/coin-icon/RainbowCoinIcon'; import * as lang from '@/languages'; import { checkForPendingSwap } from '../helpers/checkForPendingSwap'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; const TransactionMastheadHeight = android ? 153 : 135; @@ -128,7 +128,7 @@ function CurrencyTile({ } }); } - }, []); + }, [accountName, address, addressContact?.nickname]); useEffect(() => { if (!addressAccount?.image && (fetchedEnsName || addressContact?.ens)) { diff --git a/src/state/appSessions/index.test.ts b/src/state/appSessions/index.test.ts index 6ff71512df9..02e8cf06905 100644 --- a/src/state/appSessions/index.test.ts +++ b/src/state/appSessions/index.test.ts @@ -1,4 +1,4 @@ -import { Network } from '@/networks/types'; +import { ChainId } from '@/networks/types'; import { useAppSessionsStore } from '.'; const UNISWAP_HOST = 'uniswap.org'; @@ -15,13 +15,13 @@ test('should be able to add session', async () => { url: UNISWAP_URL, host: UNISWAP_HOST, address: ADDRESS_1, - network: Network.mainnet, + chainId: ChainId.mainnet, }); expect(useAppSessionsStore.getState().appSessions).toStrictEqual({ [UNISWAP_HOST]: { url: UNISWAP_URL, host: UNISWAP_HOST, - sessions: { [ADDRESS_1]: Network.mainnet }, + sessions: { [ADDRESS_1]: ChainId.mainnet }, activeSessionAddress: ADDRESS_1, }, }); @@ -33,13 +33,13 @@ test('should be able to add session to an existent host', async () => { url: UNISWAP_URL, host: UNISWAP_HOST, address: ADDRESS_2, - network: Network.arbitrum, + chainId: ChainId.arbitrum, }); expect(useAppSessionsStore.getState().appSessions).toStrictEqual({ [UNISWAP_HOST]: { url: UNISWAP_URL, host: UNISWAP_HOST, - sessions: { [ADDRESS_1]: Network.mainnet, [ADDRESS_2]: Network.arbitrum }, + sessions: { [ADDRESS_1]: ChainId.mainnet, [ADDRESS_2]: ChainId.arbitrum }, activeSessionAddress: ADDRESS_2, }, }); @@ -51,19 +51,19 @@ test('should be able to add session to a new host', async () => { url: OPENSEA_URL, host: OPENSEA_HOST, address: ADDRESS_2, - network: Network.arbitrum, + chainId: ChainId.arbitrum, }); expect(useAppSessionsStore.getState().appSessions).toStrictEqual({ [UNISWAP_HOST]: { url: UNISWAP_URL, host: UNISWAP_HOST, - sessions: { [ADDRESS_1]: Network.mainnet, [ADDRESS_2]: Network.arbitrum }, + sessions: { [ADDRESS_1]: ChainId.mainnet, [ADDRESS_2]: ChainId.arbitrum }, activeSessionAddress: ADDRESS_2, }, [OPENSEA_HOST]: { url: OPENSEA_URL, host: OPENSEA_HOST, - sessions: { [ADDRESS_2]: Network.arbitrum }, + sessions: { [ADDRESS_2]: ChainId.arbitrum }, activeSessionAddress: ADDRESS_2, }, }); @@ -76,7 +76,7 @@ test('should be able to remove app session for a host', async () => { [UNISWAP_HOST]: { url: UNISWAP_URL, host: UNISWAP_HOST, - sessions: { [ADDRESS_1]: Network.mainnet, [ADDRESS_2]: Network.arbitrum }, + sessions: { [ADDRESS_1]: ChainId.mainnet, [ADDRESS_2]: ChainId.arbitrum }, activeSessionAddress: ADDRESS_2, }, }); @@ -89,7 +89,7 @@ test('should be able to remove a session for a host and address', async () => { [UNISWAP_HOST]: { url: UNISWAP_URL, host: UNISWAP_HOST, - sessions: { [ADDRESS_1]: Network.mainnet }, + sessions: { [ADDRESS_1]: ChainId.mainnet }, activeSessionAddress: ADDRESS_1, }, }); @@ -101,7 +101,7 @@ test('should be able to update active session', async () => { url: UNISWAP_URL, host: UNISWAP_HOST, address: ADDRESS_2, - network: Network.arbitrum, + chainId: ChainId.arbitrum, }); updateActiveSession({ host: UNISWAP_HOST, address: ADDRESS_1 }); expect(useAppSessionsStore.getState().appSessions[UNISWAP_HOST].activeSessionAddress).toStrictEqual(ADDRESS_1); @@ -110,9 +110,9 @@ test('should be able to update active session', async () => { test('should be able to update active session network', async () => { const { updateActiveSessionNetwork } = useAppSessionsStore.getState(); - updateActiveSessionNetwork({ host: UNISWAP_HOST, network: Network.base }); + updateActiveSessionNetwork({ host: UNISWAP_HOST, chainId: ChainId.base }); const activeSessionAddress = useAppSessionsStore.getState().appSessions[UNISWAP_HOST].activeSessionAddress; - expect(useAppSessionsStore.getState().appSessions[UNISWAP_HOST].sessions[activeSessionAddress]).toStrictEqual(Network.base); + expect(useAppSessionsStore.getState().appSessions[UNISWAP_HOST].sessions[activeSessionAddress]).toStrictEqual(ChainId.base); }); test('should be able to update session network', async () => { @@ -121,9 +121,9 @@ test('should be able to update session network', async () => { updateSessionNetwork({ host: UNISWAP_HOST, address: ADDRESS_1, - network: Network.zora, + chainId: ChainId.zora, }); - expect(useAppSessionsStore.getState().appSessions[UNISWAP_HOST].sessions[ADDRESS_1]).toStrictEqual(Network.zora); + expect(useAppSessionsStore.getState().appSessions[UNISWAP_HOST].sessions[ADDRESS_1]).toStrictEqual(ChainId.zora); }); test('should be able to clear all sessions', async () => { @@ -139,14 +139,14 @@ test('should be able to check if host has an active session', async () => { url: UNISWAP_URL, host: UNISWAP_HOST, address: ADDRESS_1, - network: Network.mainnet, + chainId: ChainId.mainnet, }); const activeSession = getActiveSession({ host: UNISWAP_HOST }); expect(activeSession).toStrictEqual({ activeSessionAddress: ADDRESS_1, host: UNISWAP_HOST, sessions: { - '0x123': Network.mainnet, + '0x123': ChainId.mainnet, }, url: UNISWAP_URL, }); @@ -157,13 +157,13 @@ test('should be able to update session chain id', async () => { updateSessionNetwork({ host: UNISWAP_HOST, address: ADDRESS_1, - network: Network.arbitrum, + chainId: ChainId.arbitrum, }); expect(useAppSessionsStore.getState().appSessions).toStrictEqual({ [UNISWAP_HOST]: { url: UNISWAP_URL, host: UNISWAP_HOST, - sessions: { [ADDRESS_1]: Network.arbitrum }, + sessions: { [ADDRESS_1]: ChainId.arbitrum }, activeSessionAddress: ADDRESS_1, }, }); diff --git a/src/state/appSessions/index.ts b/src/state/appSessions/index.ts index 46ae89ee9b3..5a2ae7dd35c 100644 --- a/src/state/appSessions/index.ts +++ b/src/state/appSessions/index.ts @@ -1,25 +1,32 @@ import { Address } from 'viem'; -import { Network } from '@/networks/types'; +import { Network, ChainId, networkToIdMapping } from '@/networks/types'; import { createRainbowStore } from '../internal/createRainbowStore'; -export interface AppSession { +export interface AppSessionV0 { activeSessionAddress: Address; host: string; sessions: Record; url: string; } -export interface AppSessionsStore { +export interface AppSession { + activeSessionAddress: Address; + host: string; + sessions: Record; + url: string; +} + +export interface AppSessionsStore { appSessions: Record; getActiveSession: ({ host }: { host: string }) => AppSession; removeAddressSessions: ({ address }: { address: Address }) => void; - addSession: ({ host, address, network, url }: { host: string; address: Address; network: Network; url: string }) => void; - removeSession: ({ host, address }: { host: string; address: Address }) => { address: Address; network: Network } | null; + addSession: ({ host, address, chainId, url }: { host: string; address: Address; chainId: ChainId; url: string }) => void; + removeSession: ({ host, address }: { host: string; address: Address }) => { address: Address; chainId: ChainId } | null; removeAppSession: ({ host }: { host: string }) => void; updateActiveSession: ({ host, address }: { host: string; address: Address }) => void; - updateActiveSessionNetwork: ({ host, network }: { host: string; network: Network }) => void; - updateSessionNetwork: ({ address, host, network }: { address: Address; host: string; network: Network }) => void; + updateActiveSessionNetwork: ({ host, chainId }: { host: string; chainId: ChainId }) => void; + updateSessionNetwork: ({ address, host, chainId }: { address: Address; host: string; chainId: ChainId }) => void; clearSessions: () => void; } @@ -47,18 +54,18 @@ export const useAppSessionsStore = createRainbowStore { + addSession: ({ host, address, chainId, url }) => { const appSessions = get().appSessions; const existingSession = appSessions[host]; if (!existingSession || !existingSession.sessions) { appSessions[host] = { host, - sessions: { [address]: network }, + sessions: { [address]: chainId }, activeSessionAddress: address, url, }; } else { - appSessions[host].sessions[address] = network; + appSessions[host].sessions[address] = chainId; appSessions[host].activeSessionAddress = address; } set({ @@ -93,7 +100,7 @@ export const useAppSessionsStore = createRainbowStore { + updateActiveSessionNetwork: ({ host, chainId }) => { const appSessions = get().appSessions; const appSession = appSessions[host] || {}; set({ @@ -130,13 +137,13 @@ export const useAppSessionsStore = createRainbowStore { + updateSessionNetwork: ({ host, address, chainId }) => { const appSessions = get().appSessions; const appSession = appSessions[host]; if (!appSession) return; @@ -147,7 +154,7 @@ export const useAppSessionsStore = createRainbowStore { + if (version === 0) { + const oldState = persistedState as AppSessionsStore; + const appSessions: AppSessionsStore['appSessions'] = {}; + for (const [host, session] of Object.entries(oldState.appSessions)) { + const sessions = session.sessions; + const newSessions = Object.keys(sessions).reduce( + (acc, addr) => { + const address = addr as Address; + const network = sessions[address]; + acc[address] = networkToIdMapping[network]; + return acc as Record; + }, + {} as Record + ); + appSessions[host] = { + activeSessionAddress: session.activeSessionAddress, + host: session.host, + sessions: newSessions, + url: session.url, + }; + } + return { + ...oldState, + appSessions, + }; + } + return persistedState as AppSessionsStore; + }, } ); diff --git a/src/state/assets/userAssets.ts b/src/state/assets/userAssets.ts index 87dbe97b7c0..0dda7203474 100644 --- a/src/state/assets/userAssets.ts +++ b/src/state/assets/userAssets.ts @@ -1,15 +1,16 @@ import { ParsedSearchAsset, UniqueId, UserAssetFilter } from '@/__swaps__/types/assets'; -import { ChainId } from '@/__swaps__/types/chains'; -import { getIsHardhatConnected } from '@/handlers/web3'; import { Address } from 'viem'; import { RainbowError, logger } from '@/logger'; import store from '@/redux/store'; import { ETH_ADDRESS, SUPPORTED_CHAIN_IDS, supportedNativeCurrencies } from '@/references'; import { createRainbowStore } from '@/state/internal/createRainbowStore'; import { swapsStore } from '@/state/swaps/swapsStore'; +import { ChainId } from '@/networks/types'; +import { useConnectedToHardhatStore } from '../connectedToHardhat'; const SEARCH_CACHE_MAX_ENTRIES = 50; +const escapeRegExp = (string: string) => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const getSearchQueryKey = ({ filter, searchQuery }: { filter: UserAssetFilter; searchQuery: string }) => `${filter}${searchQuery}`; const getDefaultCacheKeys = (): Set => { @@ -160,7 +161,7 @@ export const userAssetsStore = createRainbowStore( return cachedData; } else { const chainIdFilter = filter === 'all' ? null : filter; - const searchRegex = inputSearchQuery.length > 0 ? new RegExp(inputSearchQuery, 'i') : null; + const searchRegex = inputSearchQuery.length > 0 ? new RegExp(escapeRegExp(inputSearchQuery), 'i') : null; const filteredIds = Array.from( selectUserAssetIds( @@ -278,7 +279,7 @@ export const userAssetsStore = createRainbowStore( }); // Ensure all supported chains are in the map with a fallback value of 0 - SUPPORTED_CHAIN_IDS({ testnetMode: getIsHardhatConnected() }).forEach(chainId => { + SUPPORTED_CHAIN_IDS({ testnetMode: useConnectedToHardhatStore.getState().connectedToHardhat }).forEach(chainId => { if (!unsortedChainBalances.has(chainId)) { unsortedChainBalances.set(chainId, 0); idsByChain.set(chainId, []); diff --git a/src/state/connectedToHardhat/index.ts b/src/state/connectedToHardhat/index.ts new file mode 100644 index 00000000000..8d6b2c5a4ae --- /dev/null +++ b/src/state/connectedToHardhat/index.ts @@ -0,0 +1,28 @@ +import create from 'zustand'; +import { createRainbowStore } from '../internal/createRainbowStore'; + +export interface ConnectedToHardhatState { + connectedToHardhat: boolean; + setConnectedToHardhat: (connectedToHardhat: boolean) => void; + + connectedToHardhatOp: boolean; + setConnectedToHardhatOp: (connectedToHardhatOp: boolean) => void; +} + +export const useConnectedToHardhatStore = createRainbowStore( + set => ({ + connectedToHardhat: false, + setConnectedToHardhat: connectedToHardhat => { + set({ connectedToHardhat }); + }, + + connectedToHardhatOp: false, + setConnectedToHardhatOp: connectedToHardhatOp => { + set({ connectedToHardhatOp }); + }, + }), + { + storageKey: 'connectedToHardhat', + version: 0, + } +); diff --git a/src/state/internal/createRainbowStore.ts b/src/state/internal/createRainbowStore.ts index 01dbcd64012..df1a4d11df0 100644 --- a/src/state/internal/createRainbowStore.ts +++ b/src/state/internal/createRainbowStore.ts @@ -38,6 +38,11 @@ interface RainbowPersistConfig { * @default 0 */ version?: number; + /** + * A function to perform persisted state migration. + * This function will be called when persisted state versions mismatch with the one specified here. + */ + migrate?: (persistedState: unknown, version: number) => S | Promise; } /** @@ -46,7 +51,7 @@ interface RainbowPersistConfig { * @returns An object containing the persist storage and version. */ function createPersistStorage(config: RainbowPersistConfig) { - const { deserializer = defaultDeserializeState, serializer = defaultSerializeState, storageKey, version = 0 } = config; + const { deserializer = defaultDeserializeState, serializer = defaultSerializeState, storageKey, version = 0, migrate } = config; const persistStorage: PersistOptions>['storage'] = { getItem: (name: string) => { @@ -151,6 +156,7 @@ export function createRainbowStore( partialize: persistConfig.partialize || (state => state), storage: persistStorage, version, + migrate: persistConfig.migrate, }) ) ); diff --git a/src/state/nonces/index.ts b/src/state/nonces/index.ts index 8383d5ada1e..d9b41e79781 100644 --- a/src/state/nonces/index.ts +++ b/src/state/nonces/index.ts @@ -1,7 +1,7 @@ import create from 'zustand'; import { createStore } from '../internal/createStore'; -import { Network } from '@/networks/types'; -import { getProviderForNetwork } from '@/handlers/web3'; +import { Network, ChainId, networkToIdMapping } from '@/networks/types'; +import { getProvider } from '@/handlers/web3'; type NonceData = { currentNonce?: number; @@ -10,41 +10,49 @@ type NonceData = { type GetNonceArgs = { address: string; - network: Network; + chainId: ChainId; }; type UpdateNonceArgs = NonceData & GetNonceArgs; -export async function getNextNonce({ address, network }: { address: string; network: Network }) { +export async function getNextNonce({ address, chainId }: { address: string; chainId: ChainId }) { const { getNonce } = nonceStore.getState(); - const localNonceData = getNonce({ address, network }); + const localNonceData = getNonce({ address, chainId }); const localNonce = localNonceData?.currentNonce || 0; - const provider = getProviderForNetwork(network); + const provider = getProvider({ chainId }); const txCountIncludingPending = await provider.getTransactionCount(address, 'pending'); if (!localNonce && !txCountIncludingPending) return 0; const ret = Math.max(localNonce + 1, txCountIncludingPending); return ret; } -export interface CurrentNonceState { - nonces: Record>; - setNonce: ({ address, currentNonce, latestConfirmedNonce, network }: UpdateNonceArgs) => void; - getNonce: ({ address, network }: GetNonceArgs) => NonceData | null; +type NoncesV0 = { + [network in Network]: NonceData; +}; + +type Nonces = { + [chainId in ChainId]: NonceData; +}; + +export interface CurrentNonceState { + nonces: Record; + setNonce: ({ address, currentNonce, latestConfirmedNonce, chainId }: UpdateNonceArgs) => void; + getNonce: ({ address, chainId }: GetNonceArgs) => NonceData | null; clearNonces: () => void; } -export const nonceStore = createStore( +export const nonceStore = createStore>( (set, get) => ({ nonces: {}, - setNonce: ({ address, currentNonce, latestConfirmedNonce, network }) => { + setNonce: ({ address, currentNonce, latestConfirmedNonce, chainId }) => { const { nonces: oldNonces } = get(); - const addressAndChainIdNonces = oldNonces?.[address]?.[network] || {}; + const addressAndChainIdNonces = oldNonces?.[address]?.[chainId] || {}; set({ nonces: { ...oldNonces, [address]: { ...oldNonces[address], - [network]: { + [chainId]: { currentNonce: currentNonce ?? addressAndChainIdNonces?.currentNonce, latestConfirmedNonce: latestConfirmedNonce ?? addressAndChainIdNonces?.latestConfirmedNonce, }, @@ -52,9 +60,9 @@ export const nonceStore = createStore( }, }); }, - getNonce: ({ address, network }) => { + getNonce: ({ address, chainId }) => { const { nonces } = get(); - return nonces[address]?.[network] ?? null; + return nonces[address]?.[chainId] ?? null; }, clearNonces: () => { set({ nonces: {} }); @@ -63,7 +71,26 @@ export const nonceStore = createStore( { persist: { name: 'nonces', - version: 0, + version: 1, + migrate: (persistedState: unknown, version: number) => { + if (version === 0) { + const oldState = persistedState as CurrentNonceState; + const newNonces: CurrentNonceState['nonces'] = {}; + for (const [address, networkNonces] of Object.entries(oldState.nonces)) { + for (const [network, nonceData] of Object.entries(networkNonces)) { + if (!newNonces[address]) { + newNonces[address] = {} as Record; + } + newNonces[address][networkToIdMapping[network as Network]] = nonceData; + } + } + return { + ...oldState, + nonces: newNonces, + }; + } + return persistedState as CurrentNonceState; + }, }, } ); diff --git a/src/state/pendingTransactions/index.ts b/src/state/pendingTransactions/index.ts index fd5ac70063b..3eb8e2db2e5 100644 --- a/src/state/pendingTransactions/index.ts +++ b/src/state/pendingTransactions/index.ts @@ -2,8 +2,8 @@ import { RainbowTransaction, NewTransaction } from '@/entities/transactions'; import { createStore } from '../internal/createStore'; import create from 'zustand'; import { parseNewTransaction } from '@/parsers/transactions'; -import { Network } from '@/networks/types'; import { nonceStore } from '../nonces'; +import { ChainId } from '@/networks/types'; export interface PendingTransactionsState { pendingTransactions: Record; @@ -35,7 +35,7 @@ export const pendingTransactionsStore = createStore( ...currentPendingTransactions, [address]: [ ...addressPendingTransactions.filter(tx => { - if (tx.network === pendingTransaction.network) { + if (tx.chainId === pendingTransaction.chainId) { return tx.nonce !== pendingTransaction.nonce; } return true; @@ -70,11 +70,11 @@ export const usePendingTransactionsStore = create(pendingTransactionsStore); export const addNewTransaction = ({ address, - network, + chainId, transaction, }: { address: string; - network: Network; + chainId: ChainId; transaction: NewTransaction; }) => { const { addPendingTransaction } = pendingTransactionsStore.getState(); @@ -83,18 +83,18 @@ export const addNewTransaction = ({ addPendingTransaction({ address, pendingTransaction: parsedTransaction }); setNonce({ address, - network, + chainId, currentNonce: transaction.nonce, }); }; export const updateTransaction = ({ address, - network, + chainId, transaction, }: { address: string; - network: Network; + chainId: ChainId; transaction: NewTransaction; }) => { const { updatePendingTransaction } = pendingTransactionsStore.getState(); @@ -103,7 +103,7 @@ export const updateTransaction = ({ updatePendingTransaction({ address, pendingTransaction: parsedTransaction }); setNonce({ address, - network, + chainId, currentNonce: transaction.nonce, }); }; diff --git a/src/state/performance/operations.ts b/src/state/performance/operations.ts index f30133ce5c6..a042200fc26 100644 --- a/src/state/performance/operations.ts +++ b/src/state/performance/operations.ts @@ -4,6 +4,7 @@ export enum Screens { SEND = 'Send', SEND_ENS = 'SendENS', WALLETCONNECT = 'WalletConnect', + MOBILE_WALLET_PROTOCOL = 'MobileWalletProtocol', } type RouteValues = (typeof Screens)[keyof typeof Screens]; diff --git a/src/state/staleBalances/index.test.ts b/src/state/staleBalances/index.test.ts new file mode 100644 index 00000000000..b07e73acb7b --- /dev/null +++ b/src/state/staleBalances/index.test.ts @@ -0,0 +1,165 @@ +import { Address } from 'viem'; + +import { staleBalancesStore } from '.'; +import { DAI_ADDRESS, OP_ADDRESS } from '@/references'; +import { ETH_ADDRESS } from '@rainbow-me/swaps'; +import { ChainId } from '@/networks/types'; + +const TEST_ADDRESS_1 = '0xFOO'; +const TEST_ADDRESS_2 = '0xBAR'; +const THEN = Date.now() - 700000; +const WHEN = Date.now() + 60000; + +test('should be able to add asset information to the staleBalances object', async () => { + const { addStaleBalance, staleBalances } = staleBalancesStore.getState(); + expect(staleBalances).toStrictEqual({}); + addStaleBalance({ + address: TEST_ADDRESS_1, + chainId: ChainId.mainnet, + info: { + address: DAI_ADDRESS, + transactionHash: '0xFOOBAR', + expirationTime: THEN, + }, + }); + addStaleBalance({ + address: TEST_ADDRESS_1, + chainId: ChainId.mainnet, + info: { + address: ETH_ADDRESS, + transactionHash: '0xFOOBAR', + expirationTime: WHEN, + }, + }); + const newStaleBalances = staleBalancesStore.getState().staleBalances; + expect(newStaleBalances).toStrictEqual({ + [TEST_ADDRESS_1]: { + [ChainId.mainnet]: { + [DAI_ADDRESS]: { + address: DAI_ADDRESS, + transactionHash: '0xFOOBAR', + expirationTime: THEN, + }, + [ETH_ADDRESS]: { + address: ETH_ADDRESS, + transactionHash: '0xFOOBAR', + expirationTime: WHEN, + }, + }, + }, + }); +}); + +test('should generate accurate stale balance query params and clear expired data - case #1', async () => { + const { getStaleBalancesQueryParam, clearExpiredData } = staleBalancesStore.getState(); + clearExpiredData(TEST_ADDRESS_1); + const queryParam = getStaleBalancesQueryParam(TEST_ADDRESS_1); + expect(queryParam).toStrictEqual(`&token=${ChainId.mainnet}.${ETH_ADDRESS}`); +}); + +test('should be able to remove expired stale balance and preserve unexpired data', async () => { + const { addStaleBalance, clearExpiredData } = staleBalancesStore.getState(); + addStaleBalance({ + address: TEST_ADDRESS_1, + chainId: ChainId.mainnet, + info: { + address: DAI_ADDRESS, + transactionHash: '0xFOOBAR', + expirationTime: THEN, + }, + }); + addStaleBalance({ + address: TEST_ADDRESS_1, + chainId: ChainId.mainnet, + info: { + address: ETH_ADDRESS as Address, + transactionHash: '0xFOOBAR', + expirationTime: WHEN, + }, + }); + clearExpiredData(TEST_ADDRESS_1); + const newStaleBalances = staleBalancesStore.getState().staleBalances; + expect(newStaleBalances).toStrictEqual({ + [TEST_ADDRESS_1]: { + [ChainId.mainnet]: { + [ETH_ADDRESS]: { + address: ETH_ADDRESS, + transactionHash: '0xFOOBAR', + expirationTime: WHEN, + }, + }, + }, + }); +}); + +test('should preserve data from other addresses when clearing expired data', async () => { + const { addStaleBalance, clearExpiredData } = staleBalancesStore.getState(); + addStaleBalance({ + address: TEST_ADDRESS_1, + chainId: ChainId.mainnet, + info: { + address: DAI_ADDRESS, + transactionHash: '0xFOOBAR', + expirationTime: THEN, + }, + }); + addStaleBalance({ + address: TEST_ADDRESS_2, + chainId: ChainId.mainnet, + info: { + address: ETH_ADDRESS as Address, + transactionHash: '0xFOOBAR', + expirationTime: WHEN, + }, + }); + clearExpiredData(TEST_ADDRESS_1); + const newStaleBalances = staleBalancesStore.getState().staleBalances; + expect(newStaleBalances).toStrictEqual({ + [TEST_ADDRESS_1]: { + [ChainId.mainnet]: { + [ETH_ADDRESS]: { + address: ETH_ADDRESS, + transactionHash: '0xFOOBAR', + expirationTime: WHEN, + }, + }, + }, + [TEST_ADDRESS_2]: { + [ChainId.mainnet]: { + [ETH_ADDRESS]: { + address: ETH_ADDRESS, + transactionHash: '0xFOOBAR', + expirationTime: WHEN, + }, + }, + }, + }); +}); + +test('should generate accurate stale balance query params and clear expired data - case #2', async () => { + const { getStaleBalancesQueryParam, clearExpiredData } = staleBalancesStore.getState(); + clearExpiredData(TEST_ADDRESS_2); + const queryParam = getStaleBalancesQueryParam(TEST_ADDRESS_2); + expect(queryParam).toStrictEqual(`&token=${ChainId.mainnet}.${ETH_ADDRESS}`); +}); + +test('should generate accurate stale balance query params and clear expired data - case #3', async () => { + const { addStaleBalance, getStaleBalancesQueryParam, clearExpiredData } = staleBalancesStore.getState(); + addStaleBalance({ + address: TEST_ADDRESS_1, + chainId: ChainId.optimism, + info: { + address: OP_ADDRESS, + transactionHash: '0xFOOBAR', + expirationTime: WHEN, + }, + }); + + clearExpiredData(TEST_ADDRESS_1); + const queryParam = getStaleBalancesQueryParam(TEST_ADDRESS_1); + expect(queryParam).toStrictEqual(`&token=${ChainId.mainnet}.${ETH_ADDRESS}&token=${ChainId.optimism}.${OP_ADDRESS}`); + + clearExpiredData(TEST_ADDRESS_2); + const queryParam2 = getStaleBalancesQueryParam(TEST_ADDRESS_2); + expect(queryParam2).toStrictEqual(`&token=${ChainId.mainnet}.${ETH_ADDRESS}`); +}); diff --git a/src/state/staleBalances/index.ts b/src/state/staleBalances/index.ts new file mode 100644 index 00000000000..8a9928aaacb --- /dev/null +++ b/src/state/staleBalances/index.ts @@ -0,0 +1,99 @@ +import { createRainbowStore } from '../internal/createRainbowStore'; + +const TIME_TO_WATCH = 600000; + +interface StaleBalanceInfo { + address: string; + expirationTime?: number; + transactionHash: string; +} + +interface StaleBalances { + [key: string]: StaleBalanceInfo; +} +interface StaleBalancesByChainId { + [key: number]: StaleBalances; +} + +export interface StaleBalancesState { + addStaleBalance: ({ address, chainId, info }: { address: string; chainId: number; info: StaleBalanceInfo }) => void; + clearExpiredData: (address: string) => void; + getStaleBalancesQueryParam: (address: string) => string; + staleBalances: Record; +} + +export const staleBalancesStore = createRainbowStore( + (set, get) => ({ + addStaleBalance: ({ address, chainId, info }: { address: string; chainId: number; info: StaleBalanceInfo }) => { + set(state => { + const { staleBalances } = state; + const staleBalancesForUser = staleBalances[address] || {}; + const staleBalancesForChain = staleBalancesForUser[chainId] || {}; + const newStaleBalancesForChain = { + ...staleBalancesForChain, + [info.address]: { + ...info, + expirationTime: info.expirationTime || Date.now() + TIME_TO_WATCH, + }, + }; + const newStaleBalancesForUser = { + ...staleBalancesForUser, + [chainId]: newStaleBalancesForChain, + }; + return { + staleBalances: { + ...staleBalances, + [address]: newStaleBalancesForUser, + }, + }; + }); + }, + clearExpiredData: (address: string) => { + set(state => { + const { staleBalances } = state; + const staleBalancesForUser = staleBalances[address] || {}; + const newStaleBalancesForUser: StaleBalancesByChainId = { + ...staleBalancesForUser, + }; + for (const c of Object.keys(staleBalancesForUser)) { + const chainId = parseInt(c, 10); + const newStaleBalancesForChain = { + ...(staleBalancesForUser[chainId] || {}), + }; + for (const staleBalance of Object.values(newStaleBalancesForChain)) { + if (typeof staleBalance.expirationTime === 'number' && staleBalance.expirationTime <= Date.now()) { + delete newStaleBalancesForChain[staleBalance.address]; + } + } + newStaleBalancesForUser[chainId] = newStaleBalancesForChain; + } + return { + staleBalances: { + ...staleBalances, + [address]: newStaleBalancesForUser, + }, + }; + }); + }, + getStaleBalancesQueryParam: (address: string) => { + let queryStringFragment = ''; + const { staleBalances } = get(); + const staleBalancesForUser = staleBalances[address]; + for (const c of Object.keys(staleBalancesForUser)) { + const chainId = parseInt(c, 10); + const staleBalancesForChain = staleBalancesForUser[chainId]; + for (const staleBalance of Object.values(staleBalancesForChain)) { + if (typeof staleBalance.expirationTime === 'number') { + queryStringFragment += `&token=${chainId}.${staleBalance.address}`; + } + } + } + return queryStringFragment; + }, + staleBalances: {}, + }), + { + storageKey: 'staleBalances', + version: 0, + } +); diff --git a/src/state/swaps/swapsStore.ts b/src/state/swaps/swapsStore.ts index 8f3c4917fea..df49c0f32a2 100644 --- a/src/state/swaps/swapsStore.ts +++ b/src/state/swaps/swapsStore.ts @@ -2,7 +2,7 @@ import { MIN_FLASHBOTS_PRIORITY_FEE } from '@/__swaps__/screens/Swap/constants'; import { getCustomGasSettings, setCustomMaxPriorityFee } from '@/__swaps__/screens/Swap/hooks/useCustomGas'; import { getSelectedGasSpeed } from '@/__swaps__/screens/Swap/hooks/useSelectedGas'; import { ExtendedAnimatedAssetWithColors, ParsedSearchAsset } from '@/__swaps__/types/assets'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId } from '@/networks/types'; import { GasSpeed } from '@/__swaps__/types/gas'; import { RecentSwap } from '@/__swaps__/types/swap'; import { getCachedGasSuggestions } from '@/__swaps__/utils/meteorology'; diff --git a/src/state/sync/UserAssetsSync.tsx b/src/state/sync/UserAssetsSync.tsx index 8d547f2f8eb..4c8c379c775 100644 --- a/src/state/sync/UserAssetsSync.tsx +++ b/src/state/sync/UserAssetsSync.tsx @@ -1,25 +1,25 @@ -import { memo } from 'react'; import { Address } from 'viem'; import { useAccountSettings } from '@/hooks'; import { userAssetsStore } from '@/state/assets/userAssets'; import { useSwapsStore } from '@/state/swaps/swapsStore'; import { selectUserAssetsList, selectorFilterByUserChains } from '@/__swaps__/screens/Swap/resources/_selectors/assets'; import { ParsedSearchAsset } from '@/__swaps__/types/assets'; -import { ChainId } from '@/__swaps__/types/chains'; -import { getIsHardhatConnected } from '@/handlers/web3'; import { useUserAssets } from '@/__swaps__/screens/Swap/resources/assets'; +import { ChainId } from '@/networks/types'; +import { useConnectedToHardhatStore } from '../connectedToHardhat'; export const UserAssetsSync = function UserAssetsSync() { const { accountAddress: currentAddress, nativeCurrency: currentCurrency } = useAccountSettings(); const userAssetsWalletAddress = userAssetsStore(state => state.associatedWalletAddress); const isSwapsOpen = useSwapsStore(state => state.isSwapsOpen); + const { connectedToHardhat } = useConnectedToHardhatStore(); useUserAssets( { address: currentAddress as Address, currency: currentCurrency, - testnetMode: getIsHardhatConnected(), + testnetMode: connectedToHardhat, }, { enabled: !isSwapsOpen || userAssetsWalletAddress !== currentAddress, diff --git a/src/storage/index.ts b/src/storage/index.ts index 824c7288ce7..14446bfbe4c 100644 --- a/src/storage/index.ts +++ b/src/storage/index.ts @@ -3,6 +3,7 @@ import { MMKV } from 'react-native-mmkv'; import { Account, Cards, Campaigns, Device, Review } from '@/storage/schema'; import { EthereumAddress, RainbowTransaction } from '@/entities'; import { Network } from '@/networks/types'; +import { SecureStorage } from '@coinbase/mobile-wallet-protocol-host'; /** * Generic storage class. DO NOT use this directly. Instead, use the exported @@ -12,8 +13,8 @@ export class Storage { protected sep = ':'; protected store: MMKV; - constructor({ id }: { id: string }) { - this.store = new MMKV({ id }); + constructor({ id, encryptionKey }: { id: string; encryptionKey?: string }) { + this.store = new MMKV({ id, encryptionKey }); } /** @@ -50,6 +51,13 @@ export class Storage { this.store.delete(scopes.join(this.sep)); } + /** + * Clear all values from storage + */ + clear() { + this.store.clearAll(); + } + /** * Remove many values from the same storage scope by keys * @@ -59,6 +67,21 @@ export class Storage { removeMany(scopes: [...Scopes], keys: Key[]) { keys.forEach(key => this.remove([...scopes, key])); } + + /** + * Encrypt the storage with a new key + * @param newEncryptionKey - The new encryption key + */ + encrypt(newEncryptionKey: string): void { + this.store.recrypt(newEncryptionKey); + } + + /** + * Remove encryption from the storage + */ + removeEncryption(): void { + this.store.recrypt(undefined); + } } /** @@ -88,3 +111,27 @@ export const cards = new Storage<[], Cards>({ id: 'cards' }); export const identifier = new Storage<[], { identifier: string }>({ id: 'identifier', }); + +/** + * Mobile Wallet Protocol storage + * + * @todo - fix any type here + */ +const mwpStorage = new Storage<[], { [key: string]: string }>({ id: 'mwp', encryptionKey: process.env.MWP_ENCRYPTION_KEY }); + +export const mwp: SecureStorage = { + get: async function (key: string): Promise { + const dataJson = mwpStorage.get([key]); + if (dataJson === undefined) { + return undefined; + } + return Promise.resolve(JSON.parse(dataJson) as T); + }, + set: async function (key: string, value: T): Promise { + const encoded = JSON.stringify(value); + mwpStorage.set([key], encoded); + }, + remove: async function (key: string): Promise { + mwpStorage.remove([key]); + }, +}; diff --git a/src/styles/colors.ts b/src/styles/colors.ts index 89edb3c587b..8985cda6999 100644 --- a/src/styles/colors.ts +++ b/src/styles/colors.ts @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { globalColors } from '@/design-system'; import currentColors from '../theme/currentColors'; import { memoFn } from '../utils/memoFn'; +import { ChainId } from '@/networks/types'; export type Colors = ReturnType; @@ -186,18 +187,18 @@ const getColorsByTheme = (darkMode?: boolean) => { }; let networkColors = { - arbitrum: '#2D374B', - base: '#0052FF', - goerli: '#f6c343', - gnosis: '#479E9C', - mainnet: '#25292E', - optimism: '#FF4040', - polygon: '#8247E5', - bsc: '#F0B90B', - zora: '#2B5DF0', - avalanche: '#E84142', - degen: '#A36EFD', - blast: '#25292E', + [ChainId.arbitrum]: '#2D374B', + [ChainId.base]: '#0052FF', + [ChainId.goerli]: '#f6c343', + [ChainId.gnosis]: '#479E9C', + [ChainId.mainnet]: '#25292E', + [ChainId.optimism]: '#FF4040', + [ChainId.polygon]: '#8247E5', + [ChainId.bsc]: '#F0B90B', + [ChainId.zora]: '#2B5DF0', + [ChainId.avalanche]: '#E84142', + [ChainId.degen]: '#A36EFD', + [ChainId.blast]: '#25292E', }; let gradients = { @@ -328,18 +329,18 @@ const getColorsByTheme = (darkMode?: boolean) => { }; networkColors = { - arbitrum: '#ADBFE3', - base: '#3979FF', - goerli: '#f6c343', - gnosis: '#479E9C', - mainnet: '#E0E8FF', - optimism: '#FF6A6A', - polygon: '#A275EE', - bsc: '#F0B90B', - zora: '#6183F0', - avalanche: '#FF5D5E', - degen: '#A36EFD', - blast: '#FCFC03', + [ChainId.arbitrum]: '#ADBFE3', + [ChainId.base]: '#3979FF', + [ChainId.goerli]: '#f6c343', + [ChainId.gnosis]: '#479E9C', + [ChainId.mainnet]: '#E0E8FF', + [ChainId.optimism]: '#FF6A6A', + [ChainId.polygon]: '#A275EE', + [ChainId.bsc]: '#F0B90B', + [ChainId.zora]: '#6183F0', + [ChainId.avalanche]: '#FF5D5E', + [ChainId.degen]: '#A36EFD', + [ChainId.blast]: '#FCFC03', }; } diff --git a/src/utils/deviceUtils.ts b/src/utils/deviceUtils.ts index efa5290c1f5..e4186a4366e 100644 --- a/src/utils/deviceUtils.ts +++ b/src/utils/deviceUtils.ts @@ -1,8 +1,10 @@ -import { Dimensions, PixelRatio, Platform } from 'react-native'; +import { Dimensions, PixelRatio, Platform, NativeModules } from 'react-native'; +const { NavbarHeight } = NativeModules; -import { IS_IOS } from '@/env'; +import { IS_ANDROID, IS_IOS } from '@/env'; const { height, width } = Dimensions.get('window'); +const scale = Dimensions.get('screen').scale; const deviceUtils = (function () { const iPhone15ProHeight = 852, @@ -39,5 +41,10 @@ const deviceUtils = (function () { export const DEVICE_WIDTH = deviceUtils.dimensions.width; export const DEVICE_HEIGHT = deviceUtils.dimensions.height; export const PIXEL_RATIO = PixelRatio.get(); - +export const NAVIGATION_BAR_HEIGHT = IS_ANDROID ? NavbarHeight.getNavigationBarHeight() / scale : 0; export default deviceUtils; + +export const isUsingButtonNavigation = () => { + if (!IS_ANDROID) return false; + return NAVIGATION_BAR_HEIGHT > 40; +}; diff --git a/src/utils/ethereumUtils.ts b/src/utils/ethereumUtils.ts index 46f3a0433db..00ac9f63577 100644 --- a/src/utils/ethereumUtils.ts +++ b/src/utils/ethereumUtils.ts @@ -24,8 +24,7 @@ import { SelectedGasFee, } from '@/entities'; import { getOnchainAssetBalance } from '@/handlers/assets'; -import { getIsHardhatConnected, getProviderForNetwork, isTestnetNetwork, toHex } from '@/handlers/web3'; -import { Network } from '@/helpers/networkTypes'; +import { getProvider, isTestnetChain, toHex } from '@/handlers/web3'; import { convertRawAmountToDecimalFormat, fromWei, greaterThan, isZero, subtract, add } from '@/helpers/utilities'; import { Navigation } from '@/navigation'; import { parseAssetNative } from '@/parsers'; @@ -43,32 +42,40 @@ import { import Routes from '@/navigation/routesNames'; import { logger, RainbowError } from '@/logger'; import { IS_IOS } from '@/env'; -import { RainbowNetworks, getNetworkObj, getNetworkObject } from '@/networks'; +import { RainbowNetworkObjects, getNetworkObject } from '@/networks'; import { externalTokenQueryKey, FormattedExternalAsset, fetchExternalToken, useExternalToken, } from '@/resources/assets/externalAssetsQuery'; -import { ChainId } from '@/__swaps__/types/chains'; +import { ChainId, Network } from '@/networks/types'; +import { AddressOrEth } from '@/__swaps__/types/assets'; +import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; -const getNetworkNativeAsset = (chainId: ChainId): ParsedAddressAsset | undefined => { +const getNetworkNativeAsset = ({ chainId }: { chainId: ChainId }) => { const nativeAssetAddress = getNetworkObject({ chainId }).nativeCurrency.address; const nativeAssetUniqueId = getUniqueId(nativeAssetAddress, chainId); return getAccountAsset(nativeAssetUniqueId); }; -export const getNativeAssetForNetwork = async (chainId: ChainId, address?: EthereumAddress): Promise => { - const network = getNetworkFromChainId(chainId); - const networkNativeAsset = getNetworkNativeAsset(chainId); +export const getNativeAssetForNetwork = async ({ + chainId, + address, +}: { + chainId: ChainId; + address?: EthereumAddress; +}): Promise => { + const networkNativeAsset = getNetworkNativeAsset({ chainId }); const { accountAddress, nativeCurrency } = store.getState().settings; const differentWallet = address?.toLowerCase() !== accountAddress?.toLowerCase(); let nativeAsset = differentWallet ? undefined : networkNativeAsset; // If the asset is on a different wallet, or not available in this wallet if (differentWallet || !nativeAsset) { - const mainnetAddress = getNetworkObject({ chainId })?.nativeCurrency?.mainnetAddress || ETH_ADDRESS; - const nativeAssetAddress = getNetworkObject({ chainId }).nativeCurrency.address; + const networkObject = getNetworkObject({ chainId }); + const mainnetAddress = networkObject?.nativeCurrency?.mainnetAddress || ETH_ADDRESS; + const nativeAssetAddress = networkObject.nativeCurrency.address as AddressOrEth; const externalAsset = await queryClient.fetchQuery( externalTokenQueryKey({ address: nativeAssetAddress, chainId, currency: nativeCurrency }), @@ -81,20 +88,20 @@ export const getNativeAssetForNetwork = async (chainId: ChainId, address?: Ether // @ts-ignore nativeAsset = { ...externalAsset, - network, - uniqueId: getUniqueId(getNetworkObject({ chainId }).nativeCurrency.address, chainId), - address: getNetworkObject({ chainId }).nativeCurrency.address, - decimals: getNetworkObject({ chainId }).nativeCurrency.decimals, - symbol: getNetworkObject({ chainId }).nativeCurrency.symbol, + network: networkObject.value, + uniqueId: getUniqueId(networkObject.nativeCurrency.address, chainId), + address: networkObject.nativeCurrency.address, + decimals: networkObject.nativeCurrency.decimals, + symbol: networkObject.nativeCurrency.symbol, }; } - const provider = getProviderForNetwork(network); + const provider = getProvider({ chainId }); if (nativeAsset) { nativeAsset.mainnet_address = mainnetAddress; - nativeAsset.address = getNetworkObject({ chainId }).nativeCurrency.address; + nativeAsset.address = networkObject.nativeCurrency.address; - const balance = await getOnchainAssetBalance(nativeAsset, address, network, provider); + const balance = await getOnchainAssetBalance(nativeAsset, address, chainId, provider); if (balance) { const assetWithBalance = { @@ -115,7 +122,7 @@ const getAsset = (accountAssets: Record, uniqueId: E const getUserAssetFromCache = (uniqueId: string) => { const { accountAddress, nativeCurrency } = store.getState().settings; - const connectedToHardhat = getIsHardhatConnected(); + const connectedToHardhat = useConnectedToHardhatStore.getState().connectedToHardhat; const cache = queryClient.getQueryCache(); @@ -168,11 +175,11 @@ const getAssetPrice = (address: EthereumAddress = ETH_ADDRESS): number => { }; export const useNativeAsset = ({ chainId }: { chainId: ChainId }) => { - let address = getNetworkObject({ chainId }).nativeCurrency?.mainnetAddress || ETH_ADDRESS; + let address = (getNetworkObject({ chainId }).nativeCurrency?.mainnetAddress || ETH_ADDRESS) as AddressOrEth; let internalChainId = ChainId.mainnet; const { nativeCurrency } = store.getState().settings; if (chainId === ChainId.avalanche || chainId === ChainId.degen) { - address = getNetworkObject({ chainId }).nativeCurrency?.address; + address = getNetworkObject({ chainId }).nativeCurrency?.address as AddressOrEth; internalChainId = chainId; } const { data: nativeAsset } = useExternalToken({ @@ -185,14 +192,14 @@ export const useNativeAsset = ({ chainId }: { chainId: ChainId }) => { }; // anotha 1 -const getPriceOfNativeAssetForNetwork = (network: Network) => { - if (network === Network.polygon) { +const getPriceOfNativeAssetForNetwork = ({ chainId }: { chainId: ChainId }) => { + if (chainId === ChainId.polygon) { return getMaticPriceUnit(); - } else if (network === Network.bsc) { + } else if (chainId === ChainId.bsc) { return getBnbPriceUnit(); - } else if (network === Network.avalanche) { + } else if (chainId === ChainId.avalanche) { return getAvaxPriceUnit(); - } else if (network === Network.degen) { + } else if (chainId === ChainId.degen) { return getDegenPriceUnit(); } return getEthPriceUnit(); @@ -274,7 +281,7 @@ const getDataString = (func: string, arrVals: string[]) => { * @param {Number} chainId */ export const getNetworkFromChainId = (chainId: ChainId): Network => { - return RainbowNetworks.find(network => network.id === chainId)?.value || getNetworkObject({ chainId: ChainId.mainnet }).value; + return RainbowNetworkObjects.find(network => network.id === chainId)?.value || getNetworkObject({ chainId: ChainId.mainnet }).value; }; /** @@ -282,7 +289,7 @@ export const getNetworkFromChainId = (chainId: ChainId): Network => { * @param {Number} chainId */ const getNetworkNameFromChainId = (chainId: ChainId): string => { - return RainbowNetworks.find(network => network.id === chainId)?.name || getNetworkObject({ chainId: ChainId.mainnet }).name; + return RainbowNetworkObjects.find(network => network.id === chainId)?.name || getNetworkObject({ chainId: ChainId.mainnet }).name; }; /** @@ -290,20 +297,20 @@ const getNetworkNameFromChainId = (chainId: ChainId): string => { * @param {String} network */ const getChainIdFromNetwork = (network?: Network): ChainId => { - return network ? getNetworkObj(network).id : ChainId.mainnet; + return RainbowNetworkObjects.find(networkObject => networkObject.value === network)?.id || ChainId.mainnet; }; /** * @desc get etherscan host from network string * @param {String} network */ -function getEtherscanHostForNetwork(chainId: ChainId): string { +function getEtherscanHostForNetwork({ chainId }: { chainId: ChainId }): string { const base_host = 'etherscan.io'; const networkObject = getNetworkObject({ chainId }); const blockExplorer = networkObject.blockExplorers?.default?.url; const network = networkObject.network as Network; - if (network && isTestnetNetwork(network)) { + if (network && isTestnetChain({ chainId })) { return `${network}.${base_host}`; } else { return blockExplorer || base_host; @@ -375,11 +382,11 @@ export const getFirstTransactionTimestamp = async (address: EthereumAddress): Pr return timestamp ? timestamp * 1000 : undefined; }; -function getBlockExplorer(chainId: ChainId) { +function getBlockExplorer({ chainId }: { chainId: ChainId }) { return getNetworkObject({ chainId }).blockExplorers?.default.name || 'etherscan'; } -function openAddressInBlockExplorer(address: EthereumAddress, chainId: ChainId) { +function openAddressInBlockExplorer({ address, chainId }: { address: EthereumAddress; chainId: ChainId }) { const explorer = getNetworkObject({ chainId })?.blockExplorers?.default?.url; Linking.openURL(`${explorer}/address/${address}`); } @@ -425,7 +432,7 @@ async function parseEthereumUrl(data: string) { if (!functionName) { // Send native asset const chainId = getChainIdFromNetwork(network); - asset = getNetworkNativeAsset(chainId); + asset = getNetworkNativeAsset({ chainId }); // @ts-ignore if (!asset || asset?.balance.amount === 0) { @@ -469,17 +476,17 @@ export const getUniqueIdNetwork = (address: EthereumAddress, network: Network) = export const getUniqueId = (address: EthereumAddress, chainId: ChainId) => `${address}_${chainId}`; -export const getAddressAndChainIdFromUniqueId = (uniqueId: string): { address: EthereumAddress; chainId: ChainId } => { +export const getAddressAndChainIdFromUniqueId = (uniqueId: string): { address: AddressOrEth; chainId: ChainId } => { const parts = uniqueId.split('_'); // If the unique ID does not contain '_', it's a mainnet address if (parts.length === 1) { - return { address: parts[0], chainId: ChainId.mainnet }; + return { address: parts[0] as AddressOrEth, chainId: ChainId.mainnet }; } // If the unique ID contains '_', the last part is the network and the rest is the address const network = parts[1] as Network; // Assuming the last part is a valid Network enum value - const address = parts[0]; + const address = parts[0] as AddressOrEth; const chainId = getChainIdFromNetwork(network); return { address, chainId }; @@ -530,13 +537,13 @@ const calculateL1FeeOptimism = async (tx: RainbowTransaction, provider: Provider const getBasicSwapGasLimit = (chainId: ChainId) => { switch (chainId) { - case getChainIdFromNetwork(Network.arbitrum): + case ChainId.arbitrum: return ethUnits.basic_swap_arbitrum; - case getChainIdFromNetwork(Network.polygon): + case ChainId.polygon: return ethUnits.basic_swap_polygon; - case getChainIdFromNetwork(Network.bsc): + case ChainId.bsc: return ethUnits.basic_swap_bsc; - case getChainIdFromNetwork(Network.optimism): + case ChainId.optimism: return ethUnits.basic_swap_optimism; default: return ethUnits.basic_swap; diff --git a/src/utils/formatDate.ts b/src/utils/formatDate.ts new file mode 100644 index 00000000000..8bb40dc1897 --- /dev/null +++ b/src/utils/formatDate.ts @@ -0,0 +1,28 @@ +import * as i18n from '@/languages'; + +export const formatDate = (dateString: string) => { + const date = new Date(dateString); + const now = new Date(); + const diffTime = Math.abs(now.getTime() - date.getTime()); + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + const diffWeeks = Math.floor(diffDays / 7); + const diffMonths = Math.floor(diffDays / 30.44); + + if (diffDays === 0) { + return i18n.t(i18n.l.walletconnect.simulation.formatted_dates.today); + } else if (diffDays === 1) { + return `${diffDays} ${i18n.t(i18n.l.walletconnect.simulation.formatted_dates.day_ago)}`; + } else if (diffDays < 7) { + return `${diffDays} ${i18n.t(i18n.l.walletconnect.simulation.formatted_dates.days_ago)}`; + } else if (diffWeeks === 1) { + return `${diffWeeks} ${i18n.t(i18n.l.walletconnect.simulation.formatted_dates.week_ago)}`; + } else if (diffDays < 30.44) { + return `${diffWeeks} ${i18n.t(i18n.l.walletconnect.simulation.formatted_dates.weeks_ago)}`; + } else if (diffMonths === 1) { + return `${diffMonths} ${i18n.t(i18n.l.walletconnect.simulation.formatted_dates.month_ago)}`; + } else if (diffDays < 365.25) { + return `${diffMonths} ${i18n.t(i18n.l.walletconnect.simulation.formatted_dates.months_ago)}`; + } else { + return date.toLocaleString('default', { month: 'short', year: 'numeric' }); + } +}; diff --git a/src/utils/getTokenMetadata.ts b/src/utils/getTokenMetadata.ts deleted file mode 100644 index fe376441c80..00000000000 --- a/src/utils/getTokenMetadata.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { TokenMetadata } from '@/entities/tokens'; -import { omitFlatten } from '@/helpers/utilities'; -import { rainbowTokenList } from '@/references'; - -export default function getTokenMetadata(tokenAddress: string | undefined): Omit | undefined { - return undefined; -} diff --git a/src/utils/getUrlForTrustIconFallback.ts b/src/utils/getUrlForTrustIconFallback.ts index c248639dfc9..49030949001 100644 --- a/src/utils/getUrlForTrustIconFallback.ts +++ b/src/utils/getUrlForTrustIconFallback.ts @@ -1,18 +1,19 @@ +import { ChainId } from '@/networks/types'; import { EthereumAddress } from '@/entities'; -import { Network } from '@/networks/types'; +import ethereumUtils from './ethereumUtils'; -export default function getUrlForTrustIconFallback(address: EthereumAddress, network: Network): string | null { +export default function getUrlForTrustIconFallback(address: EthereumAddress, chainId: ChainId): string | null { if (!address) return null; let networkPath = 'ethereum'; - switch (network) { - case Network.mainnet: + switch (chainId) { + case ChainId.mainnet: networkPath = 'ethereum'; break; - case Network.bsc: + case ChainId.bsc: networkPath = 'smartchain'; break; default: - networkPath = network; + networkPath = ethereumUtils.getNetworkFromChainId(chainId); } return `https://rainbowme-res.cloudinary.com/image/upload/assets/${networkPath}/${address}.png`; } diff --git a/src/utils/index.ts b/src/utils/index.ts index 7e70e27cf5d..aad92e7e346 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -12,7 +12,6 @@ export { default as ethereumUtils } from './ethereumUtils'; export { default as formatURLForDisplay } from './formatURLForDisplay'; export { default as gasUtils } from './gas'; export { default as getDominantColorFromImage } from './getDominantColorFromImage'; -export { default as getTokenMetadata } from './getTokenMetadata'; export { getUniqueTokenFormat, getUniqueTokenType } from './uniqueTokens'; export { default as getUrlForTrustIconFallback } from './getUrlForTrustIconFallback'; export { default as haptics } from './haptics'; diff --git a/src/utils/methodRegistry.ts b/src/utils/methodRegistry.ts index 082b1b7c39c..21abc903164 100644 --- a/src/utils/methodRegistry.ts +++ b/src/utils/methodRegistry.ts @@ -1,5 +1,5 @@ import { Contract } from '@ethersproject/contracts'; -import { web3Provider } from '../handlers/web3'; +import { getProvider } from '../handlers/web3'; import namesOverrides from '../references/method-names-overrides.json'; import methodRegistryABI from '../references/method-registry-abi.json'; import { metadataClient } from '@/graphql'; @@ -17,7 +17,8 @@ export const methodRegistryLookupAndParse = async (methodSignatureBytes: any, ch if (data?.contractFunction?.text) { signature = data.contractFunction.text; } else { - const registry = new Contract(METHOD_REGISTRY_ADDRESS, methodRegistryABI, web3Provider); + const provider = getProvider({ chainId }); + const registry = new Contract(METHOD_REGISTRY_ADDRESS, methodRegistryABI, provider); signature = await registry.entries(methodSignatureBytes); } diff --git a/src/utils/requestNavigationHandlers.ts b/src/utils/requestNavigationHandlers.ts index 8a6f193a480..e238b86c396 100644 --- a/src/utils/requestNavigationHandlers.ts +++ b/src/utils/requestNavigationHandlers.ts @@ -15,15 +15,263 @@ import { SEND_TRANSACTION } from './signingMethods'; import { handleSessionRequestResponse } from '@/walletConnect'; import ethereumUtils from './ethereumUtils'; import { getRequestDisplayDetails } from '@/parsers'; -import { RainbowNetworks } from '@/networks'; +import { RainbowNetworkObjects } from '@/networks'; import { maybeSignUri } from '@/handlers/imgix'; import { getActiveRoute } from '@/navigation/Navigation'; import { findWalletWithAccount } from '@/helpers/findWalletWithAccount'; import { enableActionsOnReadOnlyWallet } from '@/config'; import walletTypes from '@/helpers/walletTypes'; import watchingAlert from './watchingAlert'; +import { + EthereumAction, + isEthereumAction, + isHandshakeAction, + PersonalSignAction, + RequestMessage, + useMobileWalletProtocolHost, +} from '@coinbase/mobile-wallet-protocol-host'; +import { logger, RainbowError } from '@/logger'; +import { noop } from 'lodash'; +import { toUtf8String } from '@ethersproject/strings'; +import { BigNumber } from '@ethersproject/bignumber'; +import { Address } from 'viem'; +import { ChainId } from '@/networks/types'; + +export enum RequestSource { + WALLETCONNECT = 'walletconnect', + BROWSER = 'browser', + MOBILE_WALLET_PROTOCOL = 'mobile-wallet-protocol', +} + +// Mobile Wallet Protocol + +interface HandleMobileWalletProtocolRequestProps + extends Omit, 'message' | 'handleRequestUrl' | 'sendFailureToClient'> { + request: RequestMessage; +} + +const constructEthereumActionPayload = (action: EthereumAction) => { + if (action.method === 'eth_sendTransaction') { + const { weiValue, fromAddress, toAddress, actionSource, gasPriceInWei, ...rest } = action.params; + return [ + { + ...rest, + from: fromAddress, + to: toAddress, + value: weiValue, + }, + ]; + } + + return Object.values(action.params); +}; + +const supportedMobileWalletProtocolActions: string[] = [ + 'eth_requestAccounts', + 'eth_sendTransaction', + 'eth_signTypedData_v4', + 'personal_sign', + 'wallet_switchEthereumChain', +]; + +export const handleMobileWalletProtocolRequest = async ({ + request, + fetchClientAppMetadata, + approveHandshake, + rejectHandshake, + approveAction, + rejectAction, + session, +}: HandleMobileWalletProtocolRequestProps): Promise => { + logger.debug(`Handling Mobile Wallet Protocol request: ${request.uuid}`); + + const { selected } = store.getState().wallets; + const { accountAddress } = store.getState().settings; + + let addressToUse = accountAddress; + let chainIdToUse = ChainId.mainnet; + + const isReadOnlyWallet = selected?.type === walletTypes.readOnly; + if (isReadOnlyWallet && !enableActionsOnReadOnlyWallet) { + logger.debug('Rejecting request due to read-only wallet'); + watchingAlert(); + return Promise.reject(new Error('This wallet is read-only.')); + } + + const handleAction = async (currentIndex: number): Promise => { + const action = request.actions[currentIndex]; + logger.debug(`Handling action: ${action.kind}`); + + if (isHandshakeAction(action)) { + logger.debug(`Processing handshake action for ${action.appId}`); + const chainIds = Object.values(ChainId).filter(value => typeof value === 'number') as number[]; + const receivedTimestamp = Date.now(); -export type RequestSource = 'walletconnect' | 'browser'; + const dappMetadata = await fetchClientAppMetadata(); + return new Promise((resolve, reject) => { + const routeParams: WalletconnectApprovalSheetRouteParams = { + receivedTimestamp, + meta: { + chainIds, + dappName: dappMetadata?.appName || dappMetadata?.appUrl || action.appName || action.appIconUrl || action.appId || '', + dappUrl: dappMetadata?.appUrl || action.appId || '', + imageUrl: maybeSignUri(dappMetadata?.iconUrl || action.appIconUrl), + isWalletConnectV2: false, + peerId: '', + dappScheme: action.callback, + proposedChainId: request.account?.networkId || chainIdToUse, + proposedAddress: request.account?.address || addressToUse, + }, + source: RequestSource.MOBILE_WALLET_PROTOCOL, + timedOut: false, + callback: async (approved, chainId, address) => { + addressToUse = address; + chainIdToUse = chainId; + + if (approved) { + logger.debug(`Handshake approved for ${action.appId}`); + const success = await approveHandshake(dappMetadata); + resolve(success); + } else { + logger.debug(`Handshake rejected for ${action.appId}`); + await rejectHandshake('User rejected the handshake'); + reject('User rejected the handshake'); + } + }, + }; + + Navigation.handleAction( + Routes.WALLET_CONNECT_APPROVAL_SHEET, + routeParams, + getActiveRoute()?.name === Routes.WALLET_CONNECT_APPROVAL_SHEET + ); + }); + } else if (isEthereumAction(action)) { + logger.debug(`Processing ethereum action: ${action.method}`); + if (!supportedMobileWalletProtocolActions.includes(action.method)) { + logger.error(new RainbowError(`[handleMobileWalletProtocolRequest]: Unsupported action type ${action.method}`)); + await rejectAction(action, { + message: 'Unsupported action type', + code: 4001, + }); + return false; + } + + if (action.method === 'wallet_switchEthereumChain') { + const chainId = BigNumber.from(action.params.chainId).toNumber(); + const isSupportedChain = Object.values(ChainId).includes(chainId); + if (!isSupportedChain) { + await rejectAction(action, { + message: 'Unsupported chain', + code: 4001, + }); + return false; + } + + await approveAction(action, { value: 'null' }); + return true; + } + + // NOTE: This is a workaround to approve the eth_requestAccounts action if the previous action was a handshake action. + const previousAction = request.actions[currentIndex - 1]; + if (previousAction && isHandshakeAction(previousAction)) { + logger.debug('Approving eth_requestAccounts'); + await approveAction(action, { + value: JSON.stringify({ + chain: request.account?.chain ?? 'eth', + networkId: chainIdToUse, + address: addressToUse, + }), + }); + return true; + } + + const nativeCurrency = store.getState().settings.nativeCurrency; + + // @ts-expect-error - coinbase host protocol types are NOT correct e.g. {"data": [72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100], "type": "Buffer"} + if ((action as PersonalSignAction).params.message && (action as PersonalSignAction).params.message.type === 'Buffer') { + // @ts-expect-error - coinbase host protocol types are NOT correct e.g. {"data": [72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100], "type": "Buffer"} + const messageFromBuffer = toUtf8String(Buffer.from((action as PersonalSignAction).params.message.data, 'hex')); + (action as PersonalSignAction).params.message = messageFromBuffer; + } + + const payload = { + method: action.method, + params: constructEthereumActionPayload(action), + }; + + const displayDetails = await getRequestDisplayDetails(payload, nativeCurrency, request.account?.networkId ?? ChainId.mainnet); + const address = (action as PersonalSignAction).params.address || request.account?.address || accountAddress; + const requestWithDetails: RequestData = { + dappName: session?.dappName ?? session?.dappId ?? '', + dappUrl: session?.dappURL ?? '', + imageUrl: session?.dappImageURL ?? '', + address, + chainId: request.account?.networkId ?? ChainId.mainnet, + payload, + displayDetails, + }; + + return new Promise((resolve, reject) => { + const onSuccess = async (result: string) => { + logger.debug(`Ethereum action approved: [${action.method}]: ${result}`); + const success = await approveAction(action, { value: JSON.stringify(result) }); + resolve(success); + }; + + const onCancel = async (error?: Error) => { + if (error) { + logger.debug(`Ethereum action rejected: [${action.method}]: ${error.message}`); + await rejectAction(action, { + message: error.message, + code: 4001, + }); + reject(error.message); + } else { + logger.debug(`Ethereum action rejected: [${action.method}]: User rejected request`); + await rejectAction(action, { + message: 'User rejected request', + code: 4001, + }); + reject('User rejected request'); + } + }; + + Navigation.handleAction(Routes.CONFIRM_REQUEST, { + transactionDetails: requestWithDetails, + onSuccess, + onCancel, + onCloseScreen: noop, + chainId: request.account?.networkId ?? ChainId.mainnet, + address, + source: RequestSource.MOBILE_WALLET_PROTOCOL, + }); + }); + } else { + logger.error(new RainbowError(`[handleMobileWalletProtocolRequest]: Unsupported action type, ${action}`)); + return false; + } + }; + + const handleActions = async (actions: typeof request.actions, currentIndex: number = 0): Promise => { + if (currentIndex >= actions.length) { + logger.debug(`All actions completed successfully: ${actions.length}`); + return true; + } + + logger.debug(`Processing action ${currentIndex + 1} of ${actions.length}`); + const success = await handleAction(currentIndex); + if (success) { + return handleActions(actions, currentIndex + 1); + } else { + // stop processing if an action fails + return false; + } + }; + + // start processing actions starting at index 0 + return handleActions(request.actions); +}; // Dapp Browser @@ -35,9 +283,13 @@ export interface DappConnectionData { address?: string; } -export const handleDappBrowserConnectionPrompt = (dappData: DappConnectionData): Promise<{ chainId: number; address: string } | Error> => { +export const handleDappBrowserConnectionPrompt = ( + dappData: DappConnectionData +): Promise<{ chainId: ChainId; address: Address } | Error> => { return new Promise((resolve, reject) => { - const chainIds = RainbowNetworks.filter(network => network.enabled && network.networkType !== 'testnet').map(network => network.id); + const chainIds = RainbowNetworkObjects.filter(network => network.enabled && network.networkType !== 'testnet').map( + network => network.id + ); const receivedTimestamp = Date.now(); const routeParams: WalletconnectApprovalSheetRouteParams = { receivedTimestamp, @@ -52,7 +304,7 @@ export const handleDappBrowserConnectionPrompt = (dappData: DappConnectionData): proposedChainId: dappData.chainId, proposedAddress: dappData.address, }, - source: 'browser', + source: RequestSource.BROWSER, timedOut: false, callback: async (approved, approvedChainId, accountAddress) => { if (approved) { @@ -77,16 +329,31 @@ export const handleDappBrowserConnectionPrompt = (dappData: DappConnectionData): }); }; -export const handleDappBrowserRequest = async (request: Omit): Promise => { +const findWalletForAddress = async (address: string) => { + if (!address.trim()) { + return Promise.reject(new Error('Invalid address')); + } + const { wallets } = store.getState().wallets; - const selectedWallet = findWalletWithAccount(wallets!, request.address); - const isReadOnlyWallet = selectedWallet!.type === walletTypes.readOnly; + const selectedWallet = findWalletWithAccount(wallets!, address); + if (!selectedWallet) { + return Promise.reject(new Error('Wallet not found')); + } + + const isReadOnlyWallet = selectedWallet.type === walletTypes.readOnly; if (isReadOnlyWallet && !enableActionsOnReadOnlyWallet) { watchingAlert(); return Promise.reject(new Error('This wallet is read-only.')); } + + return selectedWallet; +}; + +export const handleDappBrowserRequest = async (request: Omit): Promise => { + await findWalletForAddress(request.address); + const nativeCurrency = store.getState().settings.nativeCurrency; - const displayDetails = getRequestDisplayDetails(request.payload, nativeCurrency, request.chainId); + const displayDetails = await getRequestDisplayDetails(request.payload, nativeCurrency, request.chainId); const requestWithDetails: RequestData = { ...request, @@ -118,7 +385,7 @@ export const handleDappBrowserRequest = async (request: Omit features.walletconnect).map(({ id }) => id); +const SUPPORTED_EVM_CHAIN_IDS = RainbowNetworkObjects.filter(({ features }) => features.walletconnect).map(({ id }) => id); const SUPPORTED_SESSION_EVENTS = ['chainChanged', 'accountsChanged']; @@ -93,7 +94,6 @@ export function maybeGoBackAndClearHasPendingRedirect({ delay = 0 }: { delay?: n /** * MAY BE UNDEFINED if WC v2 hasn't been instantiated yet */ -let syncWeb3WalletClient: Awaited> | undefined; let lastConnector: string | undefined = undefined; @@ -101,46 +101,41 @@ let walletConnectCore: WalletConnectCore | undefined; let web3WalletClient: ReturnType<(typeof Web3Wallet)['init']> | undefined; -let initPromise: ReturnType<(typeof Web3Wallet)['init']> | null = null; +let initPromise: Promise | undefined = undefined; + +let syncWeb3WalletClient: Client | undefined = undefined; export const initializeWCv2 = async () => { - walletConnectCore = new Core({ projectId: WC_PROJECT_ID }); - - web3WalletClient = Web3Wallet.init({ - core: walletConnectCore, - metadata: { - name: '🌈 Rainbow', - description: 'Rainbow makes exploring Ethereum fun and accessible 🌈', - url: 'https://rainbow.me', - icons: ['https://avatars2.githubusercontent.com/u/48327834?s=200&v=4'], - redirect: { - native: 'rainbow://wc', - universal: 'https://rnbwapp.com/wc', + if (!walletConnectCore) { + walletConnectCore = new Core({ projectId: WC_PROJECT_ID }); + } + + if (!web3WalletClient) { + // eslint-disable-next-line require-atomic-updates + web3WalletClient = Web3Wallet.init({ + core: walletConnectCore, + metadata: { + name: '🌈 Rainbow', + description: 'Rainbow makes exploring Ethereum fun and accessible 🌈', + url: 'https://rainbow.me', + icons: ['https://avatars2.githubusercontent.com/u/48327834?s=200&v=4'], + redirect: { + native: 'rainbow://wc', + universal: 'https://rnbwapp.com/wc', + }, }, - }, - }); + }); + } + return web3WalletClient; }; -// this function ensures we only initialize the client once export async function getWeb3WalletClient() { - if (!syncWeb3WalletClient) { - if (!initPromise) { - if (web3WalletClient) { - initPromise = web3WalletClient.then(client => { - syncWeb3WalletClient = client; - return client; - }); - } else { - await initializeWCv2(); - return getWeb3WalletClient(); - } - } - // Wait for the initialization promise to resolve - return initPromise; - } else { - return syncWeb3WalletClient; + if (!initPromise) { + initPromise = initializeWCv2(); } + + return initPromise; } /** @@ -270,7 +265,7 @@ export function isSupportedMethod(method: RPCMethod) { } export function isSupportedChain(chainId: number) { - return !!RainbowNetworks.find(({ id, features }) => id === chainId && features.walletconnect); + return !!RainbowNetworkObjects.find(({ id, features }) => id === chainId && features.walletconnect); } /** @@ -384,11 +379,14 @@ export async function initListeners() { events.emit('walletConnectV2SessionDeleted'); }, 500); }); +} +export async function initWalletConnectPushNotifications() { try { const token = await getFCMToken(); if (token) { + const client = await getWeb3WalletClient(); const client_id = await client.core.crypto.getClientId(); // initial subscription @@ -721,7 +719,7 @@ export async function onSessionRequest(event: SignClientTypes.EventArguments['se logger.debug(`[walletConnect]: handling request`, {}, logger.DebugContext.walletconnect); const dappNetwork = ethereumUtils.getNetworkFromChainId(chainId); - const displayDetails = getRequestDisplayDetails(event.params.request, nativeCurrency, dappNetwork); + const displayDetails = await getRequestDisplayDetails(event.params.request, nativeCurrency, dappNetwork); const peerMeta = session.peer.metadata; const metadata = await fetchDappMetadata({ url: peerMeta.url, status: true }); @@ -878,7 +876,7 @@ export async function onAuthRequest(event: Web3WalletTypes.AuthRequest) { * encapsulate reused code. */ const loadWalletAndSignMessage = async () => { - const provider = getProviderForNetwork(); + const provider = getProvider({ chainId: ChainId.arbitrum }); const wallet = await loadWallet({ address, showErrorIfNotLoaded: false, provider }); if (!wallet) { diff --git a/tsconfig.json b/tsconfig.json index 2f2c192422a..bc92fa9aef8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "module": "ESNext", "allowJs": true, "allowSyntheticDefaultImports": true, "esModuleInterop": true, @@ -29,7 +30,6 @@ "@rainbow-me/model/*": ["src/model/*"], "@rainbow-me/navigation": ["src/navigation"], "@rainbow-me/navigation/*": ["src/navigation/*"], - "@rainbow-me/networkTypes": ["./src/helpers/networkTypes"], "@rainbow-me/parsers": ["src/parsers"], "@rainbow-me/raps": ["src/raps"], "@rainbow-me/react-query": ["./src/react-query"], diff --git a/yarn.lock b/yarn.lock index 346f7b3376b..84aae03e450 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2095,6 +2095,21 @@ __metadata: languageName: node linkType: hard +"@coinbase/mobile-wallet-protocol-host@npm:0.1.7": + version: 0.1.7 + resolution: "@coinbase/mobile-wallet-protocol-host@npm:0.1.7" + dependencies: + buffer: "npm:^6.0.3" + events: "npm:^3.3.0" + react-native-url-polyfill: "npm:^1.3.0" + react-native-uuid: "npm:^2.0.1" + peerDependencies: + react: "*" + react-native: "*" + checksum: 10c0/9139b3886bec9f61e12dc5609aa86d3ea9670f9da42dd00295cf73d95c86099896a37492b60e3579440d1e75486b7d6782aa601d7e3f3ff604cb50a376f4e9ba + languageName: node + linkType: hard + "@daybrush/utils@npm:^0.11.0": version: 0.11.0 resolution: "@daybrush/utils@npm:0.11.0" @@ -7810,6 +7825,7 @@ __metadata: "@bradgarropy/use-countdown": "npm:1.4.1" "@candlefinance/faster-image": "npm:1.6.2" "@capsizecss/core": "npm:3.0.0" + "@coinbase/mobile-wallet-protocol-host": "npm:0.1.7" "@ensdomains/address-encoder": "npm:0.2.16" "@ensdomains/content-hash": "npm:2.5.7" "@ensdomains/eth-ens-namehash": "npm:2.0.15" @@ -7976,7 +7992,6 @@ __metadata: multiformats: "npm:9.6.2" nanoid: "npm:3.2.0" node-vibrant: "npm:3.2.1-alpha.1" - p-queue: "npm:7.2.0" p-wait-for: "npm:4.1.0" pako: "npm:2.0.4" parse-ms: "npm:2.1.0" @@ -13163,13 +13178,6 @@ __metadata: languageName: node linkType: hard -"eventemitter3@npm:^4.0.7": - version: 4.0.7 - resolution: "eventemitter3@npm:4.0.7" - checksum: 10c0/5f6d97cbcbac47be798e6355e3a7639a84ee1f7d9b199a07017f1d2f1e2fe236004d14fa5dfaeba661f94ea57805385e326236a6debbc7145c8877fbc0297c6b - languageName: node - linkType: hard - "events@npm:3.3.0, events@npm:^3.0.0, events@npm:^3.2.0, events@npm:^3.3.0": version: 3.3.0 resolution: "events@npm:3.3.0" @@ -19590,17 +19598,7 @@ __metadata: languageName: node linkType: hard -"p-queue@npm:7.2.0": - version: 7.2.0 - resolution: "p-queue@npm:7.2.0" - dependencies: - eventemitter3: "npm:^4.0.7" - p-timeout: "npm:^5.0.2" - checksum: 10c0/0dad31488d6afe5c27a84ed00a703eef1ed4387338e0debe8155d36172808c6ae0451be5d88a12aa41f1deb4d3583ecd19e5f6ded5f06c937b01ff828d18c6cb - languageName: node - linkType: hard - -"p-timeout@npm:^5.0.0, p-timeout@npm:^5.0.2": +"p-timeout@npm:^5.0.0": version: 5.1.0 resolution: "p-timeout@npm:5.1.0" checksum: 10c0/1b026cf9d5878c64bec4341ca9cda8ec6b8b3aea8a57885ca0fe2b35753a20d767fb6f9d3aa41e1252f42bc95432c05ea33b6b18f271fb10bfb0789591850a41 @@ -21455,6 +21453,24 @@ react-native-safe-area-view@rainbow-me/react-native-safe-area-view: languageName: node linkType: hard +"react-native-url-polyfill@npm:^1.3.0": + version: 1.3.0 + resolution: "react-native-url-polyfill@npm:1.3.0" + dependencies: + whatwg-url-without-unicode: "npm:8.0.0-3" + peerDependencies: + react-native: "*" + checksum: 10c0/d8167ad2cc17261906ffde19970279406db63d61b9ca85d85b02f5592e53a83db70aace3c1d89081ce46ddcfacbdac6a7faaa329b5235c6b980e1f533de5b318 + languageName: node + linkType: hard + +"react-native-uuid@npm:^2.0.1": + version: 2.0.2 + resolution: "react-native-uuid@npm:2.0.2" + checksum: 10c0/326678b3e1d076e7cda6db41ab3ee073695b31217a0e849f87bae3616a7f79e876fd8b5d735bee030275bddd44312eea9b6bfc5217997b2001d68a5d4aceec96 + languageName: node + linkType: hard + "react-native-version-number@npm:0.3.6": version: 0.3.6 resolution: "react-native-version-number@npm:0.3.6"